Avoiding Array Mutations
Note: This code uses Expect and Deep-Freeze libraries for testing and mutation checking respectively.
Say we want to implement a counter list application. We will need to write a few functions to operate on its state, which is an array of numbers representing the individual counters.
const addCounter = (list) => {list.push(0);return list;};const testAddCounter = () => {const listBefore = [];const listAfter = [0];deepFreeze(listBefore);expect(addCounter(listBefore)).toEqual(listAfter);};testAddCounter();console.log('All tests passed')
As this code stands now, the test fails because we can't push 0 onto a frozen object.
Instead, we need to use concat, because it doesn't modify the original object:
const addCounter = (list) => {// return list.concat([0]); // old wayreturn [...list, 0]; // ES6 way};
In this application we also want to be able to remove counters:
const removeCounter = (list, index) => {list.splice(index, 1);return list;}...const testRemoveCounter = () => {const listBefore = [0, 10, 20];const listAfter = [0, 20];expect (removeCounter(listBefore, 1)).toEqual(listAfter);};
This works, but splice is also a mutating method. We need to use slice instead:
const removeCounter = (list, index) => {// Old way://return list// .slice(0, index)// .concat(list.slice(index + 1));// ES6 way:return [...list.slice(0, index),...list.slice(index + 1)];};
Now let's implement incrementing the counter. The function will take in the array and the index of the counter that we are incrementing.
const incrementCounter = (list, index) => {list[index]++;return list;};const testIncrementCounter = () => {const listBefore = [0, 10, 20];const listAfter = [0, 11, 20];deepFreeze(listBefore);expect(incrementCounter(listBefore, 1)).toEqual(listAfter);};
This fails because we are mutating. The correct approach is similar to how we removed an item-- we will slice up to the item we want to increment, concat with a single item that we have incremented, then concat the rest of the original array.
const incrementCounter = (list, index) => {// Old way:// return list// .slice(0, index)// .concat([list[index] + 1])// .concat(list.slice(index + 1));// ES6 way:return [...list.slice(0, index),list[index] + 1,...list.slice(index + 1)];};
Avoiding Object Mutations with Object.assign() and ...spread
Like the previous example, this code uses the Expect and DeepFreeze libraries.
We are going to test a function toggleTodo() that takes a Todo item and toggles its "completed" field.
const toggleTodo = (todo) => {// Mutated version:todo.completed = !todo.completedreturn todo;}const testToggleTodo = () => {const todoBefore = {id: 0,text: 'Learn Redux',completed: false};const todoAfter = {id: 0,text: 'Learn Redux',completed: true};deepFreeze(todoBefore);expect(toggleTodo(todoBefore)).toEqual(todoAfter);};testToggleTodo();console.log('All tests passed.');
One way to do this without mutation is to copy the object with the "completed" value flipped:
const toggleTodo = (todo) => {return {id: todo.id,text: todo.text,completed: !todo.completed};}
However, if we add new properties later, we may forget to update this piece of code. This is why we should use ES6's Object.assign(). This lets you assign properties of several objects onto the target object.
const toggleTodo = (todo) => {return Object.assign({}, todo, {completed: !todo.completed});};
Note how the argument order to Object.assign() corresponds to the JavaScript assignment operator order.
The left argument is the one whose properties are going to be assigned, so we pass in an empty object ({}) because it will be mutated (remember, we don't want to mutate any existing data).
Every further argument to Object.assign() is considered a source object whose properties will be copied to the target (in this case, the target is our blank object provided as the first argument).
If there are multiple occurrences of the same property/properties, the last occurrence "wins". In this case, the { completed: !todo.completed } we specify in the third overwrites the completed contained within the todo in the second argument.
Remember that ES6 need to be transpiled (at least for the time being)...
Another option to do the same thing is with the spread operator proposed for ES7:
const toggleTodo = (todo) => {return {...todo,completed: !todo.completed};};