Search code examples
javascriptreactjsreact-reduxreact-hooksreducers

React useReducer: How to combine multiple reducers?


I'm not a Javascript expert so I wondered if anyone has an "elegant" way to combine multiple reducers to create a global state(Like Redux). A function that does not affect performance when a state updating multiple components etc..

Let's say I have a store.js

import React, { createContext, useReducer } from "react";
import Rootreducer from "./Rootreducer"

export const StoreContext = createContext();

const initialState = {
    ....
};

export const StoreProvider = props => {
  const [state, dispatch] = useReducer(Rootreducer, initialState);

  return (
    <StoreContext.Provider value={[state, dispatch]}>
      {props.children}
    <StoreContext.Provider>
  );
};

Rootreducer.js

import Reducer1 from "./Reducer1"
import Reducer2 from "./Reducer2"
import Reducer3 from "./Reducer3"
import Reducer4 from "./Reducer4"

const rootReducer = combineReducers({
Reducer1,
Reducer2,
Reducer3,
Reducer4
})

export default rootReducer;

Solution

  • Combine slice reducers (combineReducers)

    The most common approach is to let each reducer manage its own property ("slice") of the state:

    const combineReducers = (slices) => (state, action) =>
      Object.keys(slices).reduce( // use for..in loop, if you prefer it
        (acc, prop) => ({
          ...acc,
          [prop]: slices[prop](acc[prop], action),
        }),
        state
      );
    
    Example:
    import a from "./Reducer1";
    import b from "./Reducer2";
    
    const initialState = { a: {}, b: {} }; // some state for props a, b
    const rootReducer = combineReducers({ a, b });
    
    const StoreProvider = ({ children }) => {
      const [state, dispatch] = useReducer(rootReducer, initialState);
      // Important(!): memoize array value. Else all context consumers update on *every* render
      const store = React.useMemo(() => [state, dispatch], [state]);
      return (
        <StoreContext.Provider value={store}> {children} </StoreContext.Provider>
      );
    };
    

    Combine reducers in sequence

    Apply multiple reducers in sequence on state with arbitrary shape, akin to reduce-reducers:

    const reduceReducers = (...reducers) => (state, action) =>
      reducers.reduce((acc, nextReducer) => nextReducer(acc, action), state);
    
    Example:
    const rootReducer2 = reduceReducers(a, b);
    // rest like in first variant
    

    Combine multiple useReducer Hooks

    You could also combine dispatch and/or state from multiple useReducers, like:

    const combineDispatch = (...dispatches) => (action) =>
      dispatches.forEach((dispatch) => dispatch(action));
    
    Example:
    const [s1, d1] = useReducer(a, {}); // some init state {} 
    const [s2, d2] = useReducer(b, {}); // some init state {} 
    
    // don't forget to memoize again
    const combinedDispatch = React.useCallback(combineDispatch(d1, d2), [d1, d2]);
    const combinedState = React.useMemo(() => ({ s1, s2, }), [s1, s2]);
    
    // This example uses separate dispatch and state contexts for better render performance
    <DispatchContext.Provider value={combinedDispatch}>
      <StateContext.Provider value={combinedState}> {children} </StateContext.Provider>
    </DispatchContext.Provider>;
    

    In summary

    Above are the most common variants. There are also libraries like use-combined-reducers for these cases. Last, take a look at following sample combining both combineReducers and reduceReducers:

    const StoreContext = React.createContext();
    const initialState = { a: 1, b: 1 };
    
    // omit distinct action types for brevity
    const plusOneReducer = (state, _action) => state + 1;
    const timesTwoReducer = (state, _action) => state * 2;
    const rootReducer = combineReducers({
      a: reduceReducers(plusOneReducer, plusOneReducer), // aNew = aOld + 1 + 1
      b: reduceReducers(timesTwoReducer, plusOneReducer) // bNew = bOld * 2 + 1
    });
    
    const StoreProvider = ({ children }) => {
      const [state, dispatch] = React.useReducer(rootReducer, initialState);
      const store = React.useMemo(() => [state, dispatch], [state]);
      return (
        <StoreContext.Provider value={store}> {children} </StoreContext.Provider>
      );
    };
    
    const Comp = () => {
      const [globalState, globalDispatch] = React.useContext(StoreContext);
      return (
        <div>
          <p>
            a: {globalState.a}, b: {globalState.b}
          </p>
          <button onClick={globalDispatch}>Click me</button>
        </div>
      );
    };
    
    const App = () => <StoreProvider> <Comp /> </StoreProvider>
    ReactDOM.render(<App />, document.getElementById("root"));
    
    //
    // helpers
    //
    
    function combineReducers(slices) {
      return (state, action) =>
        Object.keys(slices).reduce(
          (acc, prop) => ({
            ...acc,
            [prop]: slices[prop](acc[prop], action)
          }),
          state
        )
    }
    
    function reduceReducers(...reducers){ 
      return (state, action) =>
        reducers.reduce((acc, nextReducer) => nextReducer(acc, action), state)
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
    <div id="root"></div>