You don't need mutation

Why is mutation evil™?

Mutation is at the core of the vast majority of bugs I had to deal with in my career, and I'm willing to bet it's at the core of yours too. Mutation means changing the value of something, which seems to be harmless until you're working on a team and you change something that shouldn't be changed. This kind of accident happens all the time in JavaScript and languages like it because when you call a function and pass an object to that function, you're passing a reference to it, instead of a copy. Let's see a simple example:

/**
 * We have a user object with 2 properties,
 * name and age.
 */
const user = {
	name: "Luke",
	age: 31
};

/**
 * We have a function that gives us the user with
 * the age change to the next year value (+1)
 */
const userNextYear = user => {
	user.age += 1;
	return user;
};

const nextYear = userNextYear(user);

// Luke's will be 32
console.log(`${nextYear.name}'s will be ${nextYear.age}`);

// Luke's age is 32
// oh no!
console.log(`${user.name}'s age is ${user.age}`);

Now, this is obvious because all the code is in the same place, now imagine the surprise if you are importing that function from somewhere else. Basically, this happens:

import { someUtil } from "somewhere";

const object = { foo: "bar" };

someUtil(object);

// `object` went into The Twilight Zone!
// Its value is unpredictable @_@

How can we resolve this?

There are several approaches to resolve the issues presented by mutation, some better than others. The worst one (and one of the most common solutions) is to just make a copy of the object before passing it to a function:

import { someDeepCopyUtil } from "someLibrary";
import { someUtil } from "somewhere";

const object = { foo: "bar" };
const copy = someDeepCopyUtil(object);

someUtil(copy);

// object is unaffected, yey!

The problem with this approach is that you're doing extra work everywhere instead of just avoiding mutations altogether. The other solution is to write your functions without doing mutations, just returning copies with changes on them. These types of functions are called pure functions, and avoiding mutations is what we call immutability. Going back to the first example:

const userNextYear = user => ({
	...user,
	age: user.age + 1
});

// This returns a copy of user:
userNextYear(user);

// So this still has the original value:
user.age;

This is great for small functions, that do little changes to small objects, but the problem is that this becomes super complex if the object has nested values:

const object = {
	foo: {
		bar: [0, 1, 2, 3],
		other: {
			value: "string"
		}
	}
};

const updateOtherValue = value => object => ({
	...object,
	foo: {
		...object.foo,
		other: {
			...object.foo.other,
			value
		}
	}
});

Which is obviously way more complex than just doing a mutation:

const updateOtherValue = value => object => {
	object.foo.other.value = value;
	return object;
};

Luckily for us, there is a great library that allows us to write code as we were doing mutations, but produces an immutable copy of the object, and it's called immer. This library allows us to write our updateOtherValue function like this:

import { produce } from "immer";

const updateOtherValue = value => object =>
	produce(object, draft => {
		draft.foo.other.value = value;
	});

We end up with the best of both worlds: Code as simple as with mutations, but immutable. Now let's go back to JavaScript without libraries for a second...

Things to avoid from vanilla

JavaScript itself provides some methods that actually aren't pure, so they mutate the original object. For example Array has a few methods in its prototype like push or pop that actually change the original value. So you end up with similar issues to the first example:

const array = ["foo", "bar"];
const addValue = value => array => array.push(value);

const addFooBar = addValue("foobar");

// This changes the original array:
addFooBar(array); // ["foo", "bar", "foobar"]

You can either just avoid not pure methods and functions, like this:

const array = ["foo", "bar"];
const addValue = value => array => array.concat(value);

const addFooBar = addValue("foobar");

// This returns a copy of the array
addFooBar(array); // ["foo", "bar", "foobar"]
// But the original is untouched :D

Or, going back to immer, we can just do this:

import { produce } from "immer";

const array = ["foo", "bar"];
const addValue = value => array => produce(array, draft => draft.push(value));

const addFooBar = addValue("foobar");

// Same effect as the pure approach 🎉
addValue(array);

Several sites cover the mutation functions, one of them that I recommend for arrays is this one: doesitmutate.xyz. It lists all the array methods and has a flag for the ones that produce mutations (so those are the ones you need to avoid).

One thing worth mentioning is that the DOM APIs are full of mutations, so if you want to change something dynamically on a WebApp you need to do mutations. Luckily for us, libraries like React, Preact, Vue, and others have an abstraction layer over the DOM called VDOM, that makes the DOM behave in a "pure" way by letting us update its state without having to do the mutations ourselves, consistently and safely.

Classes and mutation

So this article is in the same series as You don't need classes and is pretty close to it. Classes generally encourage saving values inside the class and changing those, so this is yet another reason to avoid classes and just use pure functions and values instead. Even if you decide to still use classes, try to avoid mutations, by returning new instances of the classes with the new values in them.

What about performance?

JavaScript and languages like it have a great garbage collector that takes care of the values you're not using any longer. In the vast majority of the cases, as soon as you create a copy of something and you don't use that something any longer, the original gets removed from memory.

Still, the cost in performance is way too low compared to the benefits that you get from never doing mutations.

Do you need mutations?

Similar to the previous post in this series, I finish with an open question for the readers of the post to really think about this: Do you need to mutate that value? Don't you have a way of resolving that issue without doing a mutation? I'm not saying this will always be the solution, but it should be the default.

Thanks for reading this and if you don't agree with something said here, just leave a comment and we can discuss it further.

See you in the next post of this series!

Disclaimer

This series is called "You don't need ...", emphasis on need, meaning that you would be fine without the thing that the post covers. This series explores alternatives, it doesn't impose them, so consider that before glancing over the post and ranting on the comment section. Keep it respectful.


About the original title

The original title was "You don't need variables", but I'm not saying that you don't need a way of storing data in memory, because you do ... I'm just saying that once you save something, you should never change that value. So yes, this article is about immutability and how great it is.

Leave a comment