Search code examples
reactjsstoredispatchrecoiljs

How to manipulate a global state outside of a React component using Recoil?


I'm using Recoil, and I'd like to access the store outside a component (get/set), from within a utility function.

More generally, how do people write re-usable functions that manipulate a global state with Recoil? Using Redux, we can dispatch events to the store directly, but I haven't found an alternative with Recoil.

Using hooks is a great developer experience, but it's hard to convert a function defined within a component to an external utility function because hooks can only be used within a component.


Solution

  • I managed to adapt https://github.com/facebookexperimental/Recoil/issues/289#issuecomment-777249693 answer and make it work with the Next.js framework. (see below usage example)

    This workaround allows to use the Recoil Root as a kind of global state. It only works well if there is only one RecoilRoot component, though.

    // RecoilExternalStatePortal.tsx
    import {
      Loadable,
      RecoilState,
      RecoilValue,
      useRecoilCallback,
      useRecoilTransactionObserver_UNSTABLE,
    } from 'recoil';
    
    /**
     * Returns a Recoil state value, from anywhere in the app.
     *
     * Can be used outside of the React tree (outside a React component), such as in utility scripts, etc.
    
     * <RecoilExternalStatePortal> must have been previously loaded in the React tree, or it won't work.
     * Initialized as a dummy function "() => null", it's reference is updated to a proper Recoil state mutator when RecoilExternalStatePortal is loaded.
     *
     * @example const lastCreatedUser = getRecoilExternalLoadable(lastCreatedUserState);
     */
    export let getRecoilExternalLoadable: <T>(
      recoilValue: RecoilValue<T>,
    ) => Loadable<T> = () => null as any;
    
    /**
     * Sets a Recoil state value, from anywhere in the app.
     *
     * Can be used outside of the React tree (outside a React component), such as in utility scripts, etc.
     *
     * <RecoilExternalStatePortal> must have been previously loaded in the React tree, or it won't work.
     * Initialized as a dummy function "() => null", it's reference is updated to a proper Recoil state mutator when RecoilExternalStatePortal is loaded.
     *
     * @example setRecoilExternalState(lastCreatedUserState, newUser)
     */
    export let setRecoilExternalState: <T>(
      recoilState: RecoilState<T>,
      valOrUpdater: ((currVal: T) => T) | T,
    ) => void = () => null as any;
    
    /**
     * Utility component allowing to use the Recoil state outside of a React component.
     *
     * It must be loaded in the _app file, inside the <RecoilRoot> component.
     * Once it's been loaded in the React tree, it allows using setRecoilExternalState and getRecoilExternalLoadable from anywhere in the app.
     *
     * @see https://github.com/facebookexperimental/Recoil/issues/289#issuecomment-777300212
     * @see https://github.com/facebookexperimental/Recoil/issues/289#issuecomment-777305884
     * @see https://recoiljs.org/docs/api-reference/core/Loadable/
     */
    export function RecoilExternalStatePortal() {
      // We need to update the getRecoilExternalLoadable every time there's a new snapshot
      // Otherwise we will load old values from when the component was mounted
      useRecoilTransactionObserver_UNSTABLE(({ snapshot }) => {
        getRecoilExternalLoadable = snapshot.getLoadable;
      });
    
      // We only need to assign setRecoilExternalState once because it's not temporally dependent like "get" is
      useRecoilCallback(({ set }) => {
        setRecoilExternalState = set;
    
        return async () => {
    
        };
      })();
    
      return <></>;
    }
    
    

    Configuration example using the Next.js framework:

    // pages/_app.tsx
    
    import {
      NextComponentType,
      NextPageContext,
    } from 'next';
    import { Router } from 'next/router';
    import React from 'react';
    import { RecoilRoot } from 'recoil';
    import { RecoilExternalStatePortal } from '../components/RecoilExternalStatePortal';
    
    type Props = {
      Component: NextComponentType<NextPageContext>; // Page component, not provided if pageProps.statusCode is 3xx or 4xx
      err?: Error; // Only defined if there was an error
      pageProps: any; // Props forwarded to the Page component
      router?: Router; // Next.js router state
    };
    
    /**
     * This file is the entry point for all pages, it initialize all pages.
     *
     * It can be executed server side or browser side.
     *
     * @see https://nextjs.org/docs/advanced-features/custom-app Custom _app
     * @see https://nextjs.org/docs/basic-features/typescript#custom-app TypeScript for _app
     */
    const App: React.FunctionComponent<Props> = (props): JSX.Element => {
      const { Component, pageProps} = props;
    
      return (
          <RecoilRoot>
            <Component {...pageProps} />
            <RecoilExternalStatePortal />
          </RecoilRoot>
      );
    };
    
    // Anywhere, e.g: src/utils/user.ts
    
    const createUser = (newUser) => {
      setRecoilExternalState(lastCreatedUserState, newUser)
    }