3

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.

Vadorequest
  • 16,593
  • 24
  • 118
  • 215
  • There is no "global" state with Recoil. You have atoms that live beneath the Recoil component graph. The only thing that ties Recoil to React is the `RecoilRoot` component. But maybe you can give some pseudo code of what you want to achieve so I can better understand the issue and what you want to do. – Johannes Klauß Feb 12 '21 at 15:32
  • That's true, even though when only using one `RecoilRoot`, one might consider it being the global store. – Vadorequest Feb 12 '21 at 16:36

3 Answers3

7

You can use recoil-nexus, which is a tiny package with code similar to the answer by Vadorequest.

https://www.npmjs.com/package/recoil-nexus

// Loading example
import { loadingState } from "../atoms/loadingState";
import { getRecoil, setRecoil } from "recoil-nexus";

export default function toggleLoading() {
  const loading = getRecoil(loadingState);
  setRecoil(loadingState, !loading);
}
Joe Gregory
  • 71
  • 1
  • 4
4

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)
}
Vadorequest
  • 16,593
  • 24
  • 118
  • 215
  • can you explain the usage a little more? – Pranta Apr 03 '21 at 19:27
  • @Pranta You can use `setRecoilExternalState(lastCreatedUserState, newUser)` from a non-react element (function, etc.). Is that clearer? Here is an actual example from an app I wrote: https://github.com/Vadorequest/rwa-faunadb-reaflow-nextjs-magic/search?q=setRecoilExternalState%28 – Vadorequest Apr 03 '21 at 21:15
  • hey, it doesn't seem to work in getServersideprops – Pranta Apr 05 '21 at 05:00
  • I wouldn't expect it to work in getServerSideProps, because the RecoilRoot component hasn't been defined yet. Your Recoil state doesn't even exist at this time, does it? Maybe it'd work if you define the `` in _app.js and call `getServerSideProps` from a page, but I'm not sure that'd work either. – Vadorequest Apr 05 '21 at 08:41
0

Some little hack without any npm package and complex things , but i'm not sure is it good way to do this or not :) but it works great.

In some HOC (high order component) define the ref and imperativeHanle

  1. Outside of component ( on top of component declaration )
// typescript version
export const errorGlobalRef = createRef<{
  setErrorObject: (errorObject: ErrorTypes) => void;
}>();

// javascript version
export const errorGlobalRef = createRef();
  1. Inside component
const [errorObject, setErrorObject] = useRecoilState(errorAtom);

//typescript version
useImperativeHandle(errorGlobalRef, () => {
  return {
    setErrorObject: (errorObject: ErrorTypes) => setErrorObject(errorObject),
  };
});

//javascritp version
useImperativeHandle(errorGlobalRef, () => {
  return {
    setErrorObject: (errorObject) => setErrorObject(errorObject),
  };
});

And import and use where you want ;) In my case:

//axios.config.ts
instance.interceptors.response.use(
  (response) => {
    return response;
  },
  async function (error) {
    const originalRequest = error.config;
    if (error?.response?.data) {
      const { data } = error.response;
      if (data) {
        // set recoil state
        errorGlobalRef.current?.setErrorObject({
          error: data.error,
          message: typeof data.message === 'string' ? [data.message] : data.message,
          statusCode: data.statusCode,
        });
      }
    }
    return Promise.reject(error);
  }
);
mirik999
  • 349
  • 3
  • 8