This content originally appeared on DEV Community and was authored by △ LUKE知る
Before you think I'm crazy, 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.
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 basically 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 accidents happen all the time in JavaScript and languages like it, because when you call a function and pass an object to that function, you're actually passing a reference to it, instead of a copy. Let's see a simple example:
// We have an user object with 2 properties, name and age
const user = {
name: "Luke",
age: 31
}
// We have a function that gives us the age of the user next year
const userNextYear = user => {
user.age += 1;
return user
}
const nextYear = userNextYear(user);
// Luke's age is 32
console.log(`${nextYear.name}'s age is ${nextYear.age}`);
// Luke's age was 32
// oh no!
console.log(`${user.name}'s age was ${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. This type 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
});
userNextYear(user); // returns a copy of user with the age changed
user.age; // Is still the original value
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 actually 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 actually 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");
addValue(array); // array was changed here to ["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");
addValue(array); // returns ["foo", "bar", "foobar"], but array has still the original value
Or, going back to immer, we can just to this:
import { produce } from "immer";
const array = ["foo", "bar"];
const addValue = value => array => produce(array, draft => draft.push(value));
const addFooBar = addValue("foobar");
addValue(array); // same effect as the pure approach
There are several sites that cover the mutation functions, one of them that I recommend for array 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 make the DOM behave in a "pure" way by letting us update it's state without having to do the mutations ourselves, and in a consistent and safe way.
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. 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 to low compared to the benefits that you get from never doing mutations.
Do you actually 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 really 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 in here, just leave a comment and we can discuss it further.
See you in the next post of this series!
This content originally appeared on DEV Community and was authored by △ LUKE知る
△ LUKE知る | Sciencx (2021-05-23T01:05:12+00:00) You don’t need variables. Retrieved from https://www.scien.cx/2021/05/23/you-dont-need-variables/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.