Nested redux reducers
Solution 1
It is perfectly fine to combine your nested reducers using combineReducers
. But there is another pattern which is really handy: nested reducers.
const initialState = {
user: null,
organisation: null,
token: null,
cypher: null,
someKey: null,
}
function authReducer(state = initialState, action) {
switch (action.type) {
case SET_ORGANISATION:
return {...state, organisation: organisationReducer(state.organisation, action)}
case SET_USER:
return {...state, user: userReducer(state.user, action)}
case SET_TOKEN:
return {...state, token: action.token}
default:
return state
}
}
In the above example, the authReducer
can forward the action to organisationReducer
and userReducer
to update some part of its state.
Solution 2
Just wanted to elaborate a bit on the very good answer @Florent gave and point out that you can also structure your app a bit differently to achieve nested reducers, by having your root reducer be combined from reducers that are also combined reducers
For example
// src/reducers/index.js
import { combineReducers } from "redux";
import auth from "./auth";
import posts from "./posts";
import pages from "./pages";
import widgets from "./widgets";
export default combineReducers({
auth,
posts,
pages,
widgets
});
// src/reducers/auth/index.js
// note src/reducers/auth is instead a directory
import { combineReducers } from "redux";
import organization from "./organization";
import user from "./user";
import security from "./security";
export default combineReducers({
user,
organization,
security
});
this assumes a bit different of a state structure. Instead, like so:
{
auth: {
user: {
firstName: 'Foo',
lastName: 'bar',
}
organisation: {
name: 'Foo Bar Co.'
phone: '1800-123-123',
},
security: {
token: 123123123,
cypher: '256',
someKey: 123
}
},
...
}
@Florent's approach would likely be better if you're unable to change the state structure, however
Solution 3
Inspired by @florent's answer, I found that you could also try this. Not necessarily better than his answer, but i think it's a bit more elegant.
function userReducer(state={}, action) {
switch (action.type) {
case SET_USERNAME:
state.name = action.name;
return state;
default:
return state;
}
}
function authReducer(state = {
token: null,
cypher: null,
someKey: null,
}, action) {
switch (action.type) {
case SET_TOKEN:
return {...state, token: action.token}
default:
// note: since state doesn't have "user",
// so it will return undefined when you access it.
// this will allow you to use default value from actually reducer.
return {...state, user: userReducer(state.user, action)}
}
}
Solution 4
Example (see attachNestedReducers
bellow)
import { attachNestedReducers } from './utils'
import { profileReducer } from './profile.reducer'
const initialState = { some: 'state' }
const userReducerFn = (state = initialState, action) => {
switch (action.type) {
default:
return state
}
}
export const userReducer = attachNestedReducers(userReducerFn, {
profile: profileReducer,
})
State object
{
some: 'state',
profile: { /* ... */ }
}
Here is the function
export function attachNestedReducers(original, reducers) {
const nestedReducerKeys = Object.keys(reducers)
return function combination(state, action) {
const nextState = original(state, action)
let hasChanged = false
const nestedState = {}
for (let i = 0; i < nestedReducerKeys.length; i++) {
const key = nestedReducerKeys[i]
const reducer = reducers[key]
const previousStateForKey = nextState[key]
const nextStateForKey = reducer(previousStateForKey, action)
nestedState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? Object.assign({}, nextState, nestedState) : nextState
}
}
Solution 5
Nested Reducers Example:
import {combineReducers} from 'redux';
export default combineReducers({
[PATH_USER_STATE]: UserReducer,
[PATH_CART_STATE]: combineReducers({
[TOGGLE_CART_DROPDOWN_STATE]: CartDropdownVisibilityReducer,
[CART_ITEMS_STATE]: CartItemsUpdateReducer
})
});
Output:
{
cart: {toggleCartDropdown: {…}, cartItems: {…}}
user: {currentUser: null}
}
AndrewMcLagan
I’m a full stack Web Developer with over fifteen years experience in the industry. I concentrate on open source languages and technologies such as PHP 5.6+, Python, Javascript EMCA6, AngularJS, React, Laravel and Symfony. My strengths would be a very strong interest in the open source community and approaching webapp development from a software design perspective rather than simply being a codemonkey. Strong understanding of basic SOLID principals and the patterns that make these principles a reality. Coming from many years experience as a contract developer, keeping up-to-date with the open source community, tools and best practices has been paramount to my career. Also, I LOVE to code!
Updated on July 05, 2022Comments
-
AndrewMcLagan almost 2 years
Is it possible to combine reducers that are nested with the following structure:
import 'user' from ... import 'organisation' from ... import 'auth' from ... // ... export default combineReducers({ auth: { combineReducers({ user, organisation, }), auth, }, posts, pages, widgets, // .. more state here });
Where the state has the structure:
{ auth: { user: { firstName: 'Foo', lastName: 'bar', } organisation: { name: 'Foo Bar Co.' phone: '1800-123-123', }, token: 123123123, cypher: '256', someKey: 123, } }
Where the
auth
reducer has the structure:{ token: 123123123, cypher: '256', someKey: 123, }
so maybe the spread operator is handy?
...auth
not sure :-( -
AndrewMcLagan about 8 yearswhoa.. ok this is good. Although does that mean i have multiple copies of
organisation
anduser
in my state tree? -
Florent about 8 yearsYes absolutely, you should definitely should this project: github.com/mweststrate/redux-todomvc. It is an optimized version of todomvc using these kind of tricks.
-
AndrewMcLagan about 8 yearsI love the idea, although it seems to complicate something that should be simple. :-(
-
Treefish Zhang over 7 yearsIn testing the top level nested reducer (for example the one handling the SET_ORGANISATION action), should one invoke the inner reducer (for example organisationReducer here) to derive the afterState, or simply hard-code the afterState based on the logic of the inner reducer?
-
w35l3y almost 7 years@Florent what if organisationReducer have a lot of actions? Will I need to duplicate "case" in the outer and inner reducers ? According to your example, the only action treated by organisationReducer is SET_ORGANISATION
-
Jamie Hutber over 6 yearsNice one, not sure why I thought this wasn't possible :O
-
David Harkness over 6 yearsThis solution seems much easier to maintain as your state grows.
-
Andre Platov over 6 years@w35l3y it sounds like you want your top level reducer (authReducer) to also be able to switch through "action groups" and then have another switch inside a child level reducer (say userReducer to handle specific "action group" actions. If so, you could introduce another field on the action object, say "subType" and have your userReducer go through different subType cases. Just make sure you weigh in on the trade-offs, as for example logging only action types might be less helpful and would require logging modifications, on the other side you might get a cleaner code base.
-
Andre Platov over 6 years@w35l3y on a different thought, you might have multiple top level reducer cases leading to the same child reducer. Then child reducer can reuse the actton.type for its own switch statement. in that way there can be some some degree of duplication. At the end of the day, these nested reducers are a nice tool to reduce visual code density caused by complex store structure. So whether subtypes and/or case duplication work best for you, go with it :)
-
user3764893 about 6 years@JosephNields Does that mean that whenever a component's prop is connected to lets say
user
state and another component's prop is connected lets say toauth
state, then both components would be rerendered as both their states change? Would not that be a duplicate rendering? In other words if we had anAuth
component which is composed of lets sayUser
component, and whenever a state change inUser
props, according to your proposal bothAuth
andUser
would rerender, right? -
Rajat Sharma about 6 yearsstate.name = action.name is wrong. it should have been "return {...state, name:action.name}"
-
Joseph Nields almost 6 years@user3764893 by default, connected components (via react-redux) only rerender if the new and old props aren't shallowly equal. This means changing auth.user would cause this to rerender
<Auth auth={auth} />
but not this:<Auth security={auth.security} />
. For this reason I think it's generally best to avoid passing entire chunks of state to your components -
papiro over 5 yearsI'm assuming that you are importing
organisationReducer
, but what if inorganizationReducer
you wantauthReducer
to update some part of its state? How do you avoid circular dependencies? -
explorer over 5 yearsThis is, indeed, bit more elegant! Thanks for sharing!
-
smirnoff about 5 yearsWould be nice to see the code for both
organisationReducer
andauthReducer
. -
Erick A. Montañez over 4 yearsThis is much better imho since it allows you to handle user-specific action types inside the userReducer instead of having them all in the authReducer. Thanks for the idea!