Freddy.
Freddy.

Reputation: 1745

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;

Upvotes: 47

Views: 55046

Answers (6)

Deepak challa
Deepak challa

Reputation: 11

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

Upvotes: 1

ousecTic
ousecTic

Reputation: 1925

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"
  //   }
  // }

Upvotes: 4

Felix M
Felix M

Reputation: 113

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

Upvotes: 0

ford04
ford04

Reputation: 74500

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>

Upvotes: 67

rajesh pillai
rajesh pillai

Reputation: 8118

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)

Upvotes: 8

Muhammad Zeeshan
Muhammad Zeeshan

Reputation: 4748

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.

Upvotes: 1

Related Questions