Simplify Redux reducers with immer

Before getting too deep in the weeds, we need to understand what immutability is. It all has to do with reference and value equality.

Reference equality: we compare references (pointers), and if the two instances are the same, then obj1 === obj2

Value equality: we compare values, and if the values are the same thenobj1.equals(obj2)

Reference equality is extremely quick and easy for a computer to do, all the computer does is check if the two objects share the same pointer (address in memory). Value equality takes a little more work because each value is compared individually. Two objects can have the same value but different references, but two objects can’t have the same reference if they have different values.

Redux is a library that provides an application state that embraces immutability. It has a single immutability state (referred to as store) and to change any value, part of the store has to be copied with the added/deleted/updated value. Then if the application needs to know if something has changed, all it has to do is compare the previous state and the current state. If they share the same reference, there’s nothing to do. If not it means something’s changed, the application needs to update what is displayed to the user. This makes the logic determining if an application should do something when data changes trivial and extremely fast.

Now this is really great, but requires changing the way things are usually done. Usually, if a value needs to be added to an array, the classic way to do this is by using the push function: myArray.push(newValue) , but beware: this changes the value equality which we don’t care about but does not change the reference equality which is what we do care about as it is what tells us something’s changed.

var a = [1, 2, 3];
var b = a;
a.push(4);
a === b; // true: reference equality is maintained

So we need to change the value of the array and its reference, and the way we do that is to make a copy (new instance, therefore new reference) and change that copy, this way the reference equality changes (something changed!). Here are two possible ways to do this:

// ES5
var a = [1, 2, 3];
var b = [].concat(a, 4); // a new array is created, it is the result of concatenating a and 4
a === b; // false, something changed!
// ES6
const a = [1, 2, 3];
const b = [...a, 4];
a === b; // false, something changed!

In Redux, this logic is done in reducers (a function that takes in a state, and returns a new one if something changes, or the same one if nothing changes), and you can’t mutate data in reducers! A new instance needs to be created for values to be modified. This way of modifying data may require developers to change their habits and it’s likely that mistakes will be made, but thankfully there are useful resources. Immutable Update Patterns is a great page detailing common mistakes and recommended recipes, and redux-immutable-state-invariant is a great Redux middleware that will log an error if an object is mutated accidentally. After a while, it becomes second nature to manipulate immutable objects, but ain’t gonna lie, it’s not easy at first and takes a bit of time getting used to.

Immer is another popular solution. It allows to “create the next immutable state tree by simply modifying the current tree”. In other words, Immer offers a simple and elegant solution that allows developers to update their store by mutating it (changing values without creating a copy/new instance).

A classic Redux reducer that modifies an array can be written like this:

const reducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case 'add': {
const { value } = payload;
return [...state, value];
}
case 'delete': {
const { index } = payload;
return [
...state.slice(0, index),
...state.slice(index + 1)
];
}
case 'update': {
const { value, index } = payload;
return [
...state.slice(0, index),
value,
...state.slice(index + 1)
];
}
default: {
return state;
}
}
};

The same reducer using Immer:

const reducer = produce((draft, action) => {
const { type, payload } = action;
switch (type) {
case 'add': {
const { value } = payload;
draft.push(value);
return draft;
}
case 'delete': {
const { index } = payload;
draft.splice(index, 1);
return draft;
}
case 'update': {
const { index, value } = payload;
draft[index] = value;
return draft;
}
default: {
return draft;
}
}
});

It’s that easy, the reducer using immer is the result of a function call to Immer’s produce function that takes in a draft object (the state that can be mutated) and an action.

To tie everything together, I created a small application that uses a store containing two arrays of strings, one is reduced in a standard Redux way (reduxItems) and one is reduced using Immer (immerItems). The entire store is also displayed in a raw format, and you can also use the Redux dev tools extension.

A simple React/Redux application using Immer

Please clap if you like the story, and leave suggestions and questions in the comments!

References:
Why is immutability required by Redux?
Reference & value equality
Why is immutability so important (or needed) in JavaScript?

Previously Paris and Geneva, currently New York. Can also be found at https://dev.to/arnaud