• A Few Notes on Composition of Reducers

    Managing the state of an application with libraries like redux is awesome. It provides a really easy way to write simple and testable code for state transitions. You only need to decide on a structure of the desired state and write a corresponding pure function.

    2026 update: The examples below intentionally use bare reducers and a tiny DOM setup because they show the composition mechanics directly. For new production Redux code, start with Redux Toolkit: configureStore for store setup and createSlice for writing reducers and actions. The mental model is still the same: split state into smaller reducers, then compose them into one root reducer.

    There is no doubt that a reducer could look super simple in the cases like the following one:

    // the reducer itself
    // the state is:
    //
    // [number]
    const counter = (state = 1, action) => {
      switch (action.type) {
        case 'INCREMENT':
          return state + 1;
        default:
          return state;
      }
    };
    
    // get elements
    const $counter = document.getElementById('counter');
    const $state = document.getElementById('state');
    
    const update = (state) => {
      $counter.innerHTML = state;
      $state.innerHTML = state.toString();
    };
    
    // get initial state
    let state = counter(undefined, { type: 'INIT' });
    // update dom
    update(state);
    
    $counter.onclick = function (e) {
      const action = { type: 'INCREMENT' };
      // update state with reducer
      state = counter(state, action);
      // update dom
      update(state);
    };

    Our main task as developers is to keep all reducers as simple as possible. Simple reducer is easily understandable and it’s not a big deal to write a couple of helpful tests for it. But the state grows bigger and becomes more and more complex as we add more cool features to our apps. We need a technique to hide this complexity somewhere.

    Every reducer is just a common pure function. No magic at all. Therefore it’s possible to write such a reducer function which will take a set of other reducers on its input to produce their results in a combination of some sort. Let’s look at a few examples:

    Combine a set of reducers into a map of properties

    Let’s assume that our app needs two different counters at the same time. It would be great to code something like that (with magic combineReducers function):

    // the reducer itself
    // the state is:
    //
    // [number]
    const counter = (state = 1, action) => {
      switch (action.type) {
        case 'INCREMENT':
          return state + 1;
        default:
          return state;
      }
    };
    
    const counters = Redux.combineReducers({
      counter1: counter,
      counter2: counter,
    });
    
    // get elements
    const $counter1 = document.getElementById('counter1');
    const $counter2 = document.getElementById('counter2');
    const $state = document.getElementById('state');
    
    const update = (state) => {
      $counter1.innerHTML = state.counter1;
      $counter2.innerHTML = state.counter2;
      $state.innerHTML = JSON.stringify(state);
    };
    
    // get initial state
    let state = counters(undefined, { type: 'INIT' });
    // update dom
    update(state);
    
    $counter1.onclick = function (e) {
      const action = { type: 'INCREMENT' };
      // update state with reducer
      state = counters(state, action);
      // update dom
      update(state);
    };
    
    $counter2.onclick = function (e) {
      const action = { type: 'INCREMENT' };
      // update state with reducer
      state = counters(state, action);
      // update dom
      update(state);
    };

    And it’s exactly what is possible with the standard combineReducers function from redux library. But be aware that as we use the same counter reducer for counter1 and counter2, they react the same way and counter’s values will be the same too.

    In modern Redux Toolkit code, you would usually avoid that accidental coupling by giving each slice its own generated action types:

    import { configureStore, createSlice } from '@reduxjs/toolkit';
    
    const createCounterSlice = (name) =>
      createSlice({
        name,
        initialState: 1,
        reducers: {
          increment: (state) => state + 1,
        },
      });
    
    const counter1 = createCounterSlice('counter1');
    const counter2 = createCounterSlice('counter2');
    
    const store = configureStore({
      reducer: {
        counter1: counter1.reducer,
        counter2: counter2.reducer,
      },
    });
    
    store.dispatch(counter1.actions.increment());
    // { counter1: 2, counter2: 1 }

    When configureStore receives an object of slice reducers, it creates the root reducer for you using the same combineReducers idea. You can still call combineReducers directly, but most apps do not need to do that at the top level anymore.

    Combine a set of reducers into an indexed storage

    Let’s imagine that we want a couple of such state structures. We must add an id parameter into the action to be able to determine which one of the states we want to change.

    // the reducer itself
    // the state is:
    //
    // [number]
    const counter = (state = 1, action) => {
      switch (action.type) {
        case 'INCREMENT':
          return state + 1;
        default:
          return state;
      }
    };
    
    const counters = (state = {}, action) => {
      // it makes no sense without an id parameter, so we shouldn't react on that
      if (!action.id) {
        return state;
      }
      // we should just proxy the action to the child reducer
      return Object.assign({}, state, {
        [action.id]: counter(state[action.id], action),
      });
    };
    
    // get elements
    const $counter1 = document.getElementById('counter1');
    const $counter2 = document.getElementById('counter2');
    const $state = document.getElementById('state');
    
    const update = (state) => {
      $counter1.innerHTML = state.counter1 || 1;
      $counter2.innerHTML = state.counter2 || 1;
      $state.innerHTML = JSON.stringify(state);
    };
    
    // get initial state
    let state = counters(undefined, { type: 'INIT', id: 'counter1' });
    state = counters(state, { type: 'INIT', id: 'counter2' });
    // update dom
    update(state);
    
    $counter1.onclick = function (e) {
      const action = { type: 'INCREMENT', id: 'counter1' };
      // update state with reducer
      state = counters(state, action);
      // update dom
      update(state);
    };
    
    $counter2.onclick = function (e) {
      const action = { type: 'INCREMENT', id: 'counter2' };
      // update state with reducer
      state = counters(state, action);
      // update dom
      update(state);
    };

    The Redux Toolkit version of the same keyed-state idea can be smaller:

    import { createSlice } from '@reduxjs/toolkit';
    
    const countersSlice = createSlice({
      name: 'counters',
      initialState: {},
      reducers: {
        incrementCounter(state, action) {
          const id = action.payload;
          state[id] = (state[id] ?? 1) + 1;
        },
      },
    });

    That assignment is safe inside a createSlice reducer because Redux Toolkit uses Immer internally. Outside of that context, reducers still need to produce immutable updates.

    Let the power of pure functions be with you.