Why do we need Redux and Higher Order Components?

The new Hooks API solves a lot of problems. But it does not solve the problem why we need Redux and Higher Order Components. This article explains why.

How to use the useReducer function

Consider a React app with independent container components: ‘Module A’, ‘Module B’, ‘Module C’ and so on. When I say independent, what I mean is Module A does not share state with Module B or Module C.

For such a React app, each of those independent modules will have a useReducer function.

function ModuleA() {
  const [state, dispatch] = useReducer(moduleAReducer);
}

function ModuleB() {
  const [state, dispatch] = useReducer(moduleBReducer);
}

The useReducer hook is well designed. Everytime, when React calls useReducer function, we get back the same dispatch function of the reducer. So, dispatch function is a candidate for storing in Context.

For each module, we have a DispatchContext which stores the dispatch of the reducer.

import ModuleADispatchContext from './ModuleADispatchContext';
import moduleAReducer from './moduleAReducer';

function ModuleA() {
  const [state, dispatch] = useReducer(moduleAReducer);

  return (
    <ModuleADispatchContext.Provider value={dispatch}>
      <CompA propA={state.propA} propB={state.propB} />
      <CompB propB={state.propB} />
    </ModuleADispatchContext.Provider>  
}

Since we have dispatch function as part of context, we can retrieve it in any child component in ModuleA like so.

import ModuleADispatchContext from './ModuleADispatchContext';

function SomeDeeplyNestedChildInModuleA(props) {
  const dispatch = useContext(ModuleADispatchContext);
}

Unfortunately, reducer state cannot be put in Context. This is because a change in reducer state will re-render all components in ModuleA. So, to get the reducer state for a deeply nested component in ModuleA, we have to resort to passing down props from ModuleA container component.

Why do we need Redux?

The below diagram shows how the React team expects us to use the useReducer hook.

useReducer hook
useReducer hook

To summarise, here are the major design constraints:

  • Divide the application into independent container components which have a useReducer hook.
  • Store the dispatch returned from the useReducer function call in a Context which is available for all components within the module.
  • Pass the props down from the container component down to presentation components within the module.

Clearly, these design constraints are not acceptable. Especially, it is not possible to divide a React app into independent modules. There are always valid cases where we need to share state across container components.

Other things which are not agreeable is that there are too many contexts. Each module has its own dispatch context. And it becomes really hard to give meaningful names for these dispatch context.

Finally, we have to pass down props from the container component all the way down to the leaf level components. Some of these leaf level components may be highly reusable. So, we need to have a way of getting to the reducer state without passing props all the way down.

How does Redux solve these problems? We have a global store which has state and dispatch. There is one dispatch and the dispatch allows all the reducers in the app to process the action. And we have one global state which is accessible to any component down in the component tree. So, we do need Redux or something like Redux.

Redux clone: react-reducer-store

So, I built a Redux clone: react-reducer-store. It is a poor man’s clone. Because it does not have any middleware support. There is support for logging. But there is nothing equivalent to redux-saga or redux-thunk or connected-react-router

What does the Redux clone do?

It has a combineReducers function similar to Redux. This function takes all reducers and composes a root reducer out of it. 

import { combineReducers } from 'react-reducer-store';
import todoReducer from './todoReducer';
import randomReducer from './randomReducer';

export default combineReducers({
    todo: todoReducer,
    random: randomReducer
});

It has a Store component which takes the reducer state and dispatch and puts them in global context.

export default function Store(props) {
    const initialState = props.rootReducer(props.initialValue || {}, { type: '__INIT__' });
    const [state, dispatch] = useReducer(props.rootReducer, initialState);
    
    return (
        <DispatchContext.Provider value={dispatch}>
            <StoreContext.Provider value={state}>
                {props.children}
            </StoreContext.Provider>
        </DispatchContext.Provider>
    );
}

And it has two hooks: useStore hook to get reducer state. And useDispatch hook to get the dispatch function. 

Our cheap redux clone is able to do a lot of work with Context API and useReducer function. The trick was to combine all reducers just like Redux does. But having the reducer state in Context gives us a familiar problem: too many component renders.

Let us say ModuleA makes an update to the root reducer state. And ModuleB is not interested in that update. Still, ModuleB also renders again. In fact, all components which are subscribed to the context renders.

Why do we need Higher Order Components?

Currently, my Redux clone or react-reducer-store does not have a way to cancel the render process after it is subscribed to context changes. Let us write a mapStateToProps function which tells me which context updates are interesting.

function mapStateToProps(state) {
    return {
        todo: state.todo
    };
}

We want to render only if there is an update to todo prop. To avoid expensive renders, we have the useMemo hook.

function ContainerComponent() {
  const globalState = useStore();
  const newProps = mapStateToProps(globalState);
  return useMemo(() => {
    // rest of ContainerComponent render
  }, Object.values(newProps));
}

So, we should write all our container components in this ugly sort of way. This code is reusable across container components. Should we have a hook? Unfortunately, usage of our hook will look pretty bad.

function ContainerComponent() {
  return useStore(mapStateToProps, renderFn);
}

The useStore hook will accept two arguments. The first argument computes props from the context or global state. If there is change to any of the props, call the second argument which is the render function. 

My personal preference is to avoid writing such a hook. It looks awful. Since React calls hooks from another component or another hook, we are left with writing a higher order component. 

So, the redux clone has a connect higher order component with a familiar API.

connect(mapStateToProps, MyComponent);

The code for the connect HOC is shown below.

import React, { useContext, useMemo } from 'react';
import StoreContext from './StoreContext';

export default function connect(mapStateToProps, component) {
    return function(props) {
        const context = useContext(StoreContext);
        const moreProps = mapStateToProps(context);
        const newProps = Object.assign({}, props, moreProps);
        return useMemo(() => component(newProps), Object.values(newProps));
    };
}

It picks only the relevant state from the context. And only if there is change to that state, we render the wrapped component.

Alternate solution using useStore hook

With the help of community support, I came up with an alternate solution to avoid Higher Order Component and wrote the useStore hook. In the new solution, we don’t store the reducer state in Context. Rather we allow components to subscribe to reducer state. 

To subscribe / unsubscribe from root reducer state changes, there are some helper functions.


let subscribers = [];

function subscribe(fn) {
    if (typeof fn === 'function' && subscribers.indexOf(fn) === -1) {
        subscribers.push(fn);
    }
}

function unsubscribe(fn) {
    const index = subscribers.indexOf(fn);
    if (index !== -1) {
        subscribers.splice(index, 1);
    }
}

function publish(state) {
    subscribers.forEach(fn => fn(state));
}

export {
    subscribe,
    unsubscribe,
    publish
};

Next, the Store component should not store reducer state in Context. Instead, whenever reducer state changes, we publish the new state to all subscribers.

export default function Store(props) {
    const initialState = props.rootReducer(props.initialValue || {}, { type: '__INIT__' });
    const [state, dispatch] = useReducer(props.rootReducer, initialState);
    useEffect(() => {
        publish(state);
    }, [state]);

    return (
        <DispatchContext.Provider value={dispatch}>
            {props.children}
        </DispatchContext.Provider>
    );
}

We subscribe to reducer state changes in useStore hook. The hook first computes relevant state from the global state using a mapContextToState function. If the new state is different from old state, it sets the new state. Whenever the useStore hook updates state, the component re-renders.

import { useEffect, useState } from 'react';
import { subscribe, unsubscribe } from './storeHelpers';
import shallowEqual from './shallowEqual';

let oldState;

export default function useStore(mapContextToState, initialState) {
    const [state, setState] = useState(initialState);
    oldState = state;

    useEffect(() => {
        subscribe(handleContextChange);
        return () => unsubscribe(handleContextChange);
    }, []);

    const handleContextChange = (context) => {
        if (typeof mapContextToState === 'function') {
            const newState = mapContextToState(context);
            if (!shallowEqual(newState, oldState)) {
                setState(newState);
            }
        } else {
            setState(context);
        }
    }

    return state;
}

Using the useStore hook works similar to the connect API. 

const todo = useStore(state => state.todo, []);

With the useStore hook, we don’t need higher order components. We need those higher order components to cancel the update (re-render) only if we are storing the global state in Context. The way we are publishing the global state changes is very similar to the redux store implementation. It just goes to show that we need something like a global store with publish / subscribe mechanism.

Conclusion

We still need Redux, not only for middleware like redux thunk and redux saga, but also for having a global state and a global dispatch. Storing the global state in a Context requires us to use a Higher Order Component to cancel too many updates happening to the connected component.

There is an alternate solution where we don’t store global state in Context. Rather we allow components to subscribe to global state changes in useStore hook. When global state changes, the Store component renders again. When it renders again, it publishes the state change to all the subscribers in various useState hooks. The subscriber functions in useState hook computes relevant state and if it is different from current state, it updates the state causing the component to re-render.

I will recommend to use the react-reducer-store for educational purposes. Maybe, to understand why we need to combine reducers or how useStore hook works. For production apps, always use the one and only Redux. The Redux maintainers are already working on a useRedux hook which will be part of react-redux in the future.

Related Posts

Leave a Reply

Your email address will not be published.