Skip to the code
cf.

combineReducers

Write combineReducers(reducers). reducers is an object whose values are reducers, functions (state, action) => nextState. Each one is in charge of the key of the state with the same name. combineReducers returns one reducer for the whole state.

The combined reducer takes (state, action), where state may be undefined; treat that as {}. It calls every reducer with its own slice, state[key], and the same action, and returns an object with one key per reducer. On the first call each reducer gets undefined for its slice, so its default state applies.

If no reducer returned a different value (compared with ===), return the state you were given, the very same object, so code watching the store can tell nothing changed. Otherwise return a new object and leave the old one untouched.

If a reducer returns undefined, throw an Error. A reducer must always return a state, even if it's null.

const count = (state = 0, action) => (action.type === "add" ? state + 1 : state);
const todos = (state = [], action) => (action.type === "todo" ? [...state, action.text] : state);
const root = combineReducers({ count, todos });
 
let state = root(undefined, { type: "@@INIT" }); // { count: 0, todos: [] }
state = root(state, { type: "add" });            // { count: 1, todos: [] }
root(state, { type: "unknown" }) === state;      // true

What the tests check

  1. builds the first state from each reducer's default
    const count = (state = 0) => state;
    const todos = (state = []) => state;
    const root = combineReducers({ count, todos });
    expect(root(undefined, { type: "@@INIT" })).toEqual({ count: 0, todos: [] });
    
  2. gives each reducer its own slice and the action
    const a = spy((state = 1) => state);
    const b = spy((state = 2) => state);
    const root = combineReducers({ a, b });
    const action = { type: "x" };
    root({ a: 10, b: 20 }, action);
    expect(a.calls[0].args[0]).toBe(10);
    expect(b.calls[0].args[0]).toBe(20);
    expect(a.calls[0].args[1]).toBe(action);
    expect(b.calls[0].args[1]).toBe(action);
    
  3. updates a changed slice and reuses the others
    const count = (state = 0, action) => (action.type === "add" ? state + 1 : state);
    const todos = (state = [], action) => (action.type === "todo" ? [...state, action.text] : state);
    const root = combineReducers({ count, todos });
    const before = root(undefined, { type: "@@INIT" });
    const after = root(before, { type: "add" });
    expect(after).toEqual({ count: 1, todos: [] });
    expect(after.todos).toBe(before.todos);
    
  4. returns the same state object when nothing changed
    const count = (state = 0, action) => (action.type === "add" ? state + 1 : state);
    const todos = (state = [], action) => (action.type === "todo" ? [...state, action.text] : state);
    const root = combineReducers({ count, todos });
    const state = root(undefined, { type: "@@INIT" });
    expect(root(state, { type: "unknown" })).toBe(state);
    
  5. returns a new object and leaves the old state untouched
    const count = (state = 0, action) => (action.type === "add" ? state + 1 : state);
    const root = combineReducers({ count });
    const state = { count: 0 };
    const next = root(state, { type: "add" });
    expect(next).not.toBe(state);
    expect(next).toEqual({ count: 1 });
    expect(state).toEqual({ count: 0 });
    
  6. throws an Error when a reducer returns undefined
    const root = combineReducers({
      ok: (state = 1) => state,
      broken: (state = 1, action) => (action.type === "break" ? undefined : state),
    });
    const state = root(undefined, { type: "@@INIT" });
    expect(state).toEqual({ ok: 1, broken: 1 });
    expect(() => root(state, { type: "break" })).toThrow(Error);
    
  7. accepts null as a state
    const user = (state = null, action) => (action.type === "logout" ? null : state);
    const root = combineReducers({ user });
    const state = root(undefined, { type: "@@INIT" });
    expect(state).toEqual({ user: null });
    expect(root({ user: { name: "Ada" } }, { type: "logout" })).toEqual({ user: null });
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).