Immutable as a helpful behaviour shaping constraint for Redux applications

Introduction

This post is aimed at developers with at least a rudimentary understanding of Redux, so if you're not quite up on it yet, take a look at the excellent documentation or the series of introductory videos by creator Dan Abramov.

Behaviour shaping constraints

Behaviour shaping constraints are a concept familiar in product design: cash machines that return the cash after the card to prevent the card being left behind, trains being unable to leave stations until the doors are closed or a reverse lockout gear stick on cars that prevents drivers from accidentally shifting into reverse whilst aiming for a forward gear. They are also common in computer science: static typing to pull errors back from run time to compile time, linting to enforce consistency of style and reduce syntax bugs, and pull requests to ensure that new features and modifications are ratified by peers or leads.

These are all clearly design features that force people to act in a certain way, usually to enforce safety of some kind.

The problem

Several months ago I switched from a standard flux implementation to Redux and realised the benefits of a single store and the reducer pattern for state manipulation.

One of the core principles of principles of Redux is that state is read-only, but given that JavaScript objects are by nature mutable, it's down to the fastidiousness of the developer to ensure that this never occurs. Since deadlines, late nights and developers unused to this particular programming model are all par for the course in software development, errors inevitably crop up, and what should be an unchanging (between actions), canonical data source for an application can easily become corrupted.

Immutable

Immutable is a library created to solve exactly this kind of problem - providing a hierarchical data container that cannot be mutated. After working with it for the last few months, and seeing the code cleanliness and predicability benefits over a plain javascript object state, its inclusion into future projects has become a no-brainer.

Outline

This content below is intended to highlight two of the most important features of Redux, some potential pitfalls with implementations that rely on POJOs for state and subsequently how these issues can be sidestepped by using Immutable.

The store

A single source of truth for all application state.

Benefits

Applications are easier to comprehend and debug, since one easy-to-parse object contains the entire data hierarchy. This should be compared to other UI frameworks for reference where state is scattered around in models, services and/or the view, meaning there is no easy way to have a bird's eye view of everything that's going on at any one point. Handy middleware, redux-logger, provides a way of seeing an application change over time by logging out each action that is dispatched as well as the state before and after the corresponding reduction occurred.

Any component can easily be supplied with any part of the state, since a single object is extremely easy to pass around. For example, disparate components such as a navigable shopping cart page and a deeply nested cart button in a complex header (displaying the number of items in the cart) can easily be supplied with the data they require. This is most commonly achieved by using the recommended ancillary library react-redux which enables the mapping of portions of the state object to component props.

Pitfalls

Accidental state mutation can cause unpredictable behaviour, since the same state objects could be shared with other components in the view hierarchy. For example, the previously mentioned shopping cart button could accidentally delete the contents of the cart. Obviously this is fairly unlikely and would probably be picked up quickly and easily, but more subtle and hard to detect changes could go unnoticed, leading to bugs. Thorough testing can protect against these kinds of occurrences, but the fact that they're possible and necessitate extra testing means there is an inherent danger.

Immutable to the rescue

Immutable data will not allow accidental state mutation, making this a non-issue. Since there is no way to make any changes to the state, the risk of any badly behaved part of the system causing unwanted modifications is ruled out.

Reducers

Synchronous, side-effect free data manipulation.

Benefits

Data manipulation is only ever performed in reducers making it simple to locate units of functionality and isolate bugs.

Each reducer has a single responsibility which means they are concise and simple to understand. The combined reducer for a particular area of the state tree may be complex, but each individual sub reducer should always be simple and responsible for one particular task.

They are always pure functions - synchronous and side effect free - making them easy to understand and easy to test. Since they never communicate with any other parts of the system, their effects are only ever felt in what they return and immediately.

Pitfalls

Note: This is not a criticism of reducers per se, they just happen to be the place in a React/Redux app where data manipulation takes place.

Accidental mutations are possible when performing data manipulations such as setting a nested property, removing an item from a nested array or changing a property of an object inside an array. Obviously these kind of errors can be protected against by thorough testing, but the fact that their occurrence is possible means danger is lurking nearby.

const state = {  
  firstName: 'John',
  lastName: 'Smith'.
  address: {
    line1: '1 My Street',
    city: 'London'
  }
}

const action = {  
  type: 'SET_USER_ADDRESS',
  payload: {
    address: {
      line1: '2 My Street',
      city: 'London'
    }
  }
}

// Bad - mutating initial state with new address
setUserAddressReducer(state, action) {  
  state.address = action.payload.address;
  return state;
}

// Good - cloning with spread operator and adding new address
setUserAddressReducer(state, action) {  
  return {
    ...state,
    address: action.payload.address
  };
}

Immutable to the rescue

Immutability makes accidentally returning mutated state impossible, meaning that the previously mentioned reducer pitfalls, as with the section concerning the store, dissolve into non-issues.

Below is a slightly more involved example of how Immutable can help make code more succinct, easier to comprehend and less error prone.

import {find, clone} from 'lodash';  
import {fromJS} from 'immutable';

// State
const state = {  
  id: '3273984829',
  orderLines: [
    {
      id: '123',
      productName: 'Apple',
      quantity: 1
    },
    {
      id: '456',
      productName: 'Banana',
      quantity: 3
    }
  ]
};

// Dispatched action
const action = {  
  type: 'SET_ORDER_LINE_QUANTITY',
  payload: {
    id: '456',
    quantity: 5
  }
}

// Mutable reducer
function setOrderLineQuantityReducer__mutable(state, action) {  
  const clonedOrderLines = clone(state.orderLines);
  const targetOrderLine = find(state.orderLines, orderLine => orderLine.id === action.payload.id);

  clonedOrderLines[state.orderLines.indexOf(targetOrderLine)] = {
    ...targetOrderLine,
    quantity: action.payload.quantity
  };

  return {
    ...state,
    orderLines: clonedOrderLines
  };
}

// Immutable reducer
function setOrderLineQuantityReducer__immutable(state, action) {  
  const orderLines = state.get('orderLines');
  const targetOrderLine = orderLines.find(orderLine => orderLine.get('id') === action.payload.id)
  return state.set('orderLines', orderLines.set(orderLines.indexOf(targetOrderLine), targetOrderLine.set('quantity', action.payload.quantity)))
}

// --------------------------------------------------
// Execution

const mutableOutput = setOrderLineQuantityReducer__mutable(state, action);  
console.log(mutableOutput);

const immutableOutput = setOrderLineQuantityReducer__immutable(fromJS(state), action);  
console.log(immutableOutput.toJS());

Extra treats

As well as this, Immutable also provides many utility methods detailed in its extensive documentation including:

  • fromJS - Deeply converts plain JavaScript objects into Immutable Maps and Lists.
  • getIn - Returns deeply nested properties, returning undefined if the property or the hierarchy doesn't exist, removing the need for ugly, nested conditionals.
  • setIn() - Facilitates the setting of data within deep object hierarchies dynamically creating intermediate objects where necessary.
  • mergeDeep() Merges deeply nested data structures.
  • is() Checks for deep equality of structure and primitives in two objects.

Conclusion

Redux offers many benefits, providing its programming model is adhered to strictly. Unfortunately, since JavaScript is by nature completely dynamic, breaking these rules is all too easy. Immutable isn't a magic wand but it does help developers sidestep some of the issues that arise when trying to implement Redux's programming model in a mutable environment by putting in place a helpful behaviour-shaping constraint.