Search code examples
javascriptreactjsreduxreact-hooksreact-redux

Making it safe to call a hook from outside of the Redux Provider, when it access the Redux store?


I have a hook useGetFooId that attempts to get a Foo ID from the Redux store with fallback logic:

  1. If the Foo ID is set in the Redux store, it returns the ID from there (but it's expected that in some cases, it's not set in the Redux store).
  2. Otherwise, if the Foo ID is not available from the Redux store, it falls back to trying to get the ID from a few other, less reliable places.

It looks something like:

const useGetFooId = () => {
  const { fooId: fooIdFromSelector } = useSelector(uiSelector);
  const fooIdFromLessReliablePlace = getFooIdFromLessReliablePlace();
  const fooIdFromVeryUnreliablePlace = getVeryUnreliableFooId();

  return fooIdFromSelector
    ?? fooIdFromLessReliablePlace
    ?? fooIdFromVeryUnreliablePlace
    ?? null;
}

Now, I want to make useGetFooId safe to call from outside of the Redux Provider. (The Foo ID is relevant to some display logic in an error boundary component at the root of the application). Right now, if it's called from outside the Provider, it gets this error: Cannot read properties of null (reading 'store').

The desired behavior is basically: If useGetFooId is called from outside of the selector, it uses the same fallback logic as if fooId was undefined in the selector (so, it should return fooIdFromLessReliablePlace ?? fooIdFromVeryUnreliablePlace ?? null).

Is there a supported way to conditionally access the Redux store only if the Provider is available, or something? I originally thought to wrap it in try/catch, but it seems as though this violates the rules of hooks: https://github.com/facebook/react/issues/16026


Solution

  • Rewrite your custom to subscribe to changes from the store using the store object directly instead of the useSelector hook which can't be called conditionally, e.g. can't be called conditionally in a try/catch.

    See store.subscribe.

    Use a useEffect hook to instantiate a listener that can select the current fooId value from the store and save it into a local React state. You can surround the store subscription logic in a try/catch.

    import { store } from '../path/to/store';
    
    const useGetFooId = () => {
      const [fooIdFromSelector, setFooIdFromSelector] = React.useState(null);
    
      React.useEffect(() => {
        try {
          const unsubscribe = store.subscribe(() => {
            const { fooId } = uiSelector(store.getState());
            setFooIdFromSelector(fooId);
          });
    
          return unsubscribe;
        } catch(error) {
          // some error happened during the store subscription
          // handle/log/ignore/etc, it's up to you
        }
      }, []);
    
      const fooIdFromLessReliablePlace = getFooIdFromLessReliablePlace();
      const fooIdFromVeryUnreliablePlace = getVeryUnreliableFooId();
    
      return fooIdFromSelector
        ?? fooIdFromLessReliablePlace
        ?? fooIdFromVeryUnreliablePlace
        ?? null;
    };