Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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;
like image 801
Freddy. Avatar asked Dec 05 '19 17:12

Freddy.


4 Answers

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>
like image 83
ford04 Avatar answered Sep 20 '22 16:09

ford04


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)

like image 39
rajesh pillai Avatar answered Sep 19 '22 16:09

rajesh pillai


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"
  //   }
  // }
like image 34
ousecTic Avatar answered Sep 19 '22 16:09

ousecTic


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.

like image 27
Muhammad Zeeshan Avatar answered Sep 17 '22 16:09

Muhammad Zeeshan