71

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
  • 13,459
  • 23
  • 91
  • 158

5 Answers5

81

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.

Florent
  • 12,310
  • 10
  • 49
  • 58
  • whoa.. ok this is good. Although does that mean i have multiple copies of `organisation` and `user` in my state tree? – AndrewMcLagan Apr 22 '16 at 07:04
  • 1
    Yes absolutely, you should definitely should this project: https://github.com/mweststrate/redux-todomvc. It is an optimized version of todomvc using these kind of tricks. – Florent Apr 22 '16 at 07:21
  • I love the idea, although it seems to complicate something that should be simple. :-( – AndrewMcLagan Apr 22 '16 at 07:22
  • 1
    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? – Treefish Zhang Dec 15 '16 at 03:42
  • 9
    @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 – w35l3y Jun 26 '17 at 13:50
  • 1
    @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 Feb 08 '18 at 16:29
  • 1
    @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 :) – Andre Platov Feb 08 '18 at 17:25
  • 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? – papiro Sep 25 '18 at 18:15
  • 4
    Would be nice to see the code for both `organisationReducer` and `authReducer`. – smirnoff Mar 20 '19 at 17:39
60

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

Joseph Nields
  • 5,527
  • 2
  • 32
  • 48
  • Nice one, not sure why I thought this wasn't possible :O – Jamie Hutber Oct 08 '17 at 21:39
  • 11
    This solution seems much easier to maintain as your state grows. – David Harkness Dec 06 '17 at 23:20
  • @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? – user3764893 Mar 07 '18 at 15:42
  • 1
    @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 `` but not this: ``. For this reason I think it's generally best to avoid passing entire chunks of state to your components – Joseph Nields May 22 '18 at 21:42
13

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)}
  }
}
D.W
  • 511
  • 5
  • 9
1

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
  }
}
iamandrewluca
  • 3,411
  • 1
  • 31
  • 38
1

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}
}
Vinayak V Naik
  • 1,291
  • 1
  • 9
  • 7