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;
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> ); };
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
useReducer
HooksYou could also combine dispatch and/or state from multiple useReducer
s, 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>;
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>
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)
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"
// }
// }
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With