Using the Context API as a way of mimicking useSelector and useDispatch with redux v5

I’m working on a React project where I’m constrained to using React Redux v5, which doesn’t include useDispatch and useSelector.

Nonetheless I really would like to have these hooks (or something like them) available in my app.
Therefore, I’ve created a wrapper component at the top level of the app which I connect using redux’s connect function.
My mapStateToProps and mapDispatchToProps then just look like this:

const mapDispatchToProps = (dispatch: DispatchType) => {
    return {
        dispatch,
    };
};

const mapStateToProps = (state: StateType) => {
    return {
        state,
    };
};

export default connect(mapStateToProps, mapDispatchToProps)(MainLayout);

In my wrapper component, I then pass the dispatch and the state into the value:

  <DispatchContext.Provider value={{ state, dispatch }}>
{children}
</DispatchContext.Provider>

Finally, I have a hook that looks like this:

const useSelectAndDispatch = () => {
    const context = useContext(DispatchContext);
    if (context === null) {
        throw new Error("Please use useDispatch within DispatchContext");
    }
    const { state, dispatch } = context;

    function select(selector) {
        return selector(state);
    }
    return { dispatch, select };
};

I then use dispatch and selector in my components via useSelectAndDispatch.

I was wondering if this is an appropriate way to go about this issue, and whether I can expect any performance problems. I am using reselect, and have a good understanding of memoization. I’m just looking for opinions, since I’ve heard that the redux team held off implementing useDispatch and useSelector for a long time because of performance issues.

Many thanks for any opinions!

>Solution :

This will cause significant peformance problems. Your mapStateToProps is using the entire state object, so every time anything changes in the state, the provider must rerender. And since the provider rerendered with a new value, so too must every component that consumes the context. In short, you will be forcing most of your app to rerender anytime anything changes.

Instead of using mapStateToProps and mapDispatchToProps, i would go back to the actual store object, and build your hooks from that. Somewhere in your app is presumably a line of code that says const store = createStore(/* some options */).

Using that store variable, you can then make some hooks. If i can assume that there’s only one store in your app, then the dispatch hook is trivial:

import { store } from 'wherever the store is created'

export const useMyDispatch = () => {
  return store.dispatch;
}

And the selector one would be something like this. It uses .subscribe to be notified when something in the store changes, and then it uses the selector to pluck out the part of the state that you care about. If nothing changed, then the old state and new state will pass an === check, and react skips rendering. If it has changed though, the component renders (only the component that called useMySelect plus its children, not the entire app)

export const useMySelector = (selector) => {
  const [value, setValue] = useState(() => {
    return selector(store.getState());
  });
  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      const newValue = selector(store.getState());
      setValue(newValue);
    });
    return unsubscribe;
  }, []);
  return value;
}

Leave a Reply