Nested redux reducers

38,172

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}
}
Share:
38,172
AndrewMcLagan
Author by

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 web­app development from a software design perspective rather than simply being a code­monkey. 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, 2022

Comments

  • AndrewMcLagan
    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
    AndrewMcLagan about 8 years
    whoa.. ok this is good. Although does that mean i have multiple copies of organisation and user in my state tree?
  • Florent
    Florent about 8 years
    Yes 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
    AndrewMcLagan about 8 years
    I love the idea, although it seems to complicate something that should be simple. :-(
  • Treefish Zhang
    Treefish Zhang over 7 years
    In 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
    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
    Jamie Hutber over 6 years
    Nice one, not sure why I thought this wasn't possible :O
  • David Harkness
    David Harkness over 6 years
    This solution seems much easier to maintain as your state grows.
  • Andre Platov
    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
    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
    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 to auth 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 an Auth component which is composed of lets say User component, and whenever a state change in User props, according to your proposal both Auth and User would rerender, right?
  • Rajat Sharma
    Rajat Sharma about 6 years
    state.name = action.name is wrong. it should have been "return {...state, name:action.name}"
  • Joseph Nields
    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
    papiro over 5 years
    I'm assuming that you are importing organisationReducer, but what if in organizationReducer you want authReducer to update some part of its state? How do you avoid circular dependencies?
  • explorer
    explorer over 5 years
    This is, indeed, bit more elegant! Thanks for sharing!
  • smirnoff
    smirnoff about 5 years
    Would be nice to see the code for both organisationReducer and authReducer.
  • Erick A. Montañez
    Erick A. Montañez over 4 years
    This 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!