skip to Main Content

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

2

Answers


  1. 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;
    };
    
    Login or Signup to reply.
  2. You only need the logic outside of the provider, and not the selector, because:

    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).

    Create a function called getFooId that optionally receives an id (id can also be undefined, and use it in the custom hook, and directly in the error boundary (id would be undefined):

    // only call other functions if the previous value is `null` or `undefined`
    const getFooId = id => id
      ?? getFooIdFromLessReliablePlace()
      ?? getVeryUnreliableFooId()
      ?? null;
    
    const useGetFooId = () => {
      const { fooId: fooIdFromSelector } = useSelector(uiSelector);
    
      return getFooId(fooIdFromSelector);
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search