React useReducer: How to combine multiple reducers?
Asked Answered
S

6

45

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;
Shirley answered 5/12, 2019 at 17:54 Comment(1)
Have you seen this previous thread it might help #55620885Gothic
V
65

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>
Varico answered 26/4, 2020 at 11:11 Comment(0)
A
8

If you simply want to achieve a combine reducer feature without any third-party library, do it as below. (REF: Redux source/code) The working code is here https://codepen.io/rajeshpillai/pen/jOPWYzL?editors=0010

I have two reducers created, one dateReducer and another counterReducer. I am using it as

const [state, dispatch] = useReducer(combineReducers({ counter: counterReducer, date: dateReducer }), initialState);

The combineReducers code

function combineReducers(reducers) {  
  return (state = {}, action) => {
    const newState = {};
    for (let key in reducers) {
      newState[key] = reducers[key](state[key], action);
    }
    return newState;
  }
}

Usage: Extract the respective state

const { counter, date } = state;

NOTE: You can add more redux like features if you wish.

The complete working code (in case codepen is down :))

const {useReducer, useEffect} = React;


function dateReducer(state, action) {
  switch(action.type) {
    case "set_date":
      return action.payload;
      break;
    default:
      return state;
  }  
}

function counterReducer(state, action) {
  console.log('cr:', state);
  switch (action.type) {
    case 'increment': {
      return state + 1;
    }
    case 'decrement': {
      return state - 1;
    }

    default:
      return state;
  }
}

function combineReducers(reducers) {  
  return (state = {}, action) => {
    const newState = {};
    for (let key in reducers) {
      newState[key] = reducers[key](state[key], action);
    }
    return newState;
  }
}

const initialState = {
  counter: 0,
  date: new Date
};

function App() {
  const [state, dispatch] = useReducer(combineReducers({
    counter: counterReducer,
    date: dateReducer 
  }), initialState);  

  console.log("state", state);
  const { counter, date } = state;

  return (
    <div className="app">
      <h3>Counter Reducer</h3>
      <div className="counter">
        <button onClick={() => 
          dispatch({ type: 'increment'})}>+          
        </button>

        <h2>{counter.toString()}</h2>
        <button onClick={() => 
             dispatch({ type: 'decrement'})}>-
        </button>
      </div>
      <hr/>
      <h3>Date Reducer</h3>
      {date.toString()}
      <button className="submit" 
          type="submit"
          onClick={() => 
             dispatch({ type: 'set_date', payload:new Date })}>
           Set Date
        </button>
    </div>
  );
}

const rootElement = document.querySelector("#root");
ReactDOM.render(<App />, rootElement);  

NOTE: This is a quick hack (for learning and demonstration purpose only)

Audrieaudris answered 14/2, 2020 at 7:30 Comment(2)
is it possible to convert this combine reducer function into a typescript function?Sailplane
This one worked for me. The reducer based one isn't updating state correctly.Late
T
3

There is a library called react combine reducer that is specifically use for combining reducer with the context api. Below is the code sample

import { useReducer } from 'react';
  import combineReducers from 'react-combine-reducers';
 
  const initialIdentity = {
    name: 'Harry'
  }
 
  const initialLocation = {
    country: 'UK',
    city: 'London'
  }
 
  const identityReducer = (state, action) => {
    switch (action.type) {
      case 'ACTION_A':
        return { ...state, name: 'Puli' };
      default: return state;
    }
  }
 
  const locationReducer = (state, action) => {
    switch (action.type) {
      case 'ACTION_B':
        return { ...state, city: 'Manchester' };
      default: return state;
    }
  }
 
  const [profileReducer, initialProfile] = combineReducers({
    identity: [identityReducer, initialIdentity],
    location: [locationReducer, initialLocation]
  });
 
  const [state, dispatch] = useReducer(profileReducer, initialProfile);
 
  console.log(state);
  // Outputs the following state:
  // {
  //   identity: {
  //     name: "Harry"
  //   },
  //   location: {
  //     country: "UK",
  //     city: "London"
  //   }
  // }
Tsan answered 16/9, 2020 at 3:21 Comment(0)
Z
1

In your rootReducer.js file you can use combineReducers from redux to combine multiple reducers. The traditional way is:

import { combineReducers } from 'redux';

const rootReducer = combineReducers({ name: nameReducer});

export default rootReducer;

You can import the rootReducer while creating the store as:

import { combineReducers } from 'redux';

let store = createStore(rootReducer);

While using useReducer hook you can pass the rootReducer to it:

const [state, dispatch] = useReducer(rootReducer, initialState);

Hope this works for you.

Zeke answered 5/12, 2019 at 18:2 Comment(8)
How is that different from what they're doing now?Selfcentered
Hooks are introduced to make react application more optimized. As it is said in the official documentation that: useReducer lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.Zeke
OP is already using useReducer. I'm asking how your code is different than what the OP already has.Selfcentered
He asked about a better approach. I showed him the both ways the scenario can be handled.Zeke
@MuhammadZeeshan would I have to use redux createStore? Ideally would like to stick with contextShirley
@Freddy.If you are using Context API then no need to use redux createStore. I just mentioned that so you can see both ways.Zeke
Never thought I could use redux combineReducers(). Is there any performance or side effects by doing it this way? well pros and cons are what I'm asking forShirley
Hooks were introduced for making performance better so in my opinion using Context API will be a better option if you are more concerned about the performance.Zeke
S
1

Instead of using useReducer use useCombineReducers() . may change this function to accept multiple parameters based on your requirement

const inti ={  count:0,  alpha:''} 

export function reducer1(state, action) {
switch (action.type) 
{
  case 'increment':
    return {...state , count: state.count + 1};
  case 'decrement':
    return {...state , count: state.count - 1};     
  default:
    return {count:0};
} }

export function reducer2(state, action) {
switch (action.type) {
  case 'add':
    return {...state , alpha: state.alpha + action.payload };
  case 'rem':
    return {...state , alpha: state.alpha + action.payload};     
  default:
    return {alpha:''};
}}

function useCombineReducers(reducer1,reducer2, init) {
const [state,setState] = useState(init);

function dispatch(action)
{
  let ns = null;
  if(action.type == 'add' || action.type=="rem")
  {        
    ns = reducer2(state,action)
  }
  else
  {
    ns = reducer1(state,action)      
  }
    setState(ns);
}  
return [state, dispatch];}

function App() {
  const [state,dispatch] = useCombineReducers(reducer1,reducer2,inti);

  return (
  <>
   <Provider >
   <Counter state ={state} dispatch={dispatch}></Counter>
   <Alpha state ={state} dispatch={dispatch}></Alpha>
   </Provider>
 </>
);   }

const Counter = (props) => {
return (
    <div style ={{Border:'10px', width:'20px'}}>
        Count : {props.state.count}

        <button onClick={()=> props.dispatch({type: 'increment'})}> + </button> 
        <button onClick={()=> props.dispatch({type: 'decrement'})}> - </button> 
    </div>
)} export default Counter

const Alpha = (props) => {
return (
    <div style ={{Border:'10px', width:'20px'}}>
        Alpha : {props.state.alpha}

        <button onClick={()=> props.dispatch({type: 'add',payload:'+'})}> + </button> 
        <button onClick={()=> props.dispatch({type: 'rem',payload:'-'})}> - </button> 
    </div>
)} export default Alpha
Stomachache answered 22/4, 2021 at 13:53 Comment(1)
Good effort, even though it's not the best architecture.Hearten
C
0

I played around a bit and were thinking about the problem as I had to handle it as well.

This might not be the best approach but I just defined my reducers as objects with key: reducer function combinations:

const counterRed = {
  increment: (oldState, action) => ({
    ...oldState,
    counter: oldState.counter + 1
  }),
  decrement: (oldState, action) => ({
    ...oldState,
    counter: oldState.counter - 1
  })
};

and

const dateRed = {
  set_date: (oldState, action) => ({ ...oldState, date: action.payload })
};

and I combined them like this:

const reducer = (oldState, action) => {
  const combinedReducers = { ...dateRed, ...counterRed };
  let newState = null;
  if (combinedReducers[action.type]) {
    newState = combinedReducers[action.type](oldState, action);
  }
  if (newState) {
    return { ...newState };
  }
  return oldState;
};

a working example can be seen here: https://codesandbox.io/s/jovial-kowalevski-25pzf?file=/src/App.js

Cannady answered 27/4, 2020 at 12:16 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.