• A Few Notes on Composition of Reducers

    5ada4a05c5b4apexels photo 355288

    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.

    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.

    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);
    };

    Let the power of pure functions be with you.

One is the pinnacle of evolution; another one is me

One on the picture is the pinnacle of evolution; another one is me: inspired developer, geek culture lover, sport and coffee addict