While JavaScript allows us to mutate objects, we might choose to not allow ourselves (and fellow programmers) to do so. One of the best examples of this in the JavaScript world today is when we're setting state in a React application. If we mutate our current state rather than a new copy of our current state, we can encounter hard-to-diagnose issues.
In this post, we roll our own immutable proxy function to prevent object mutation!
If you enjoy this post, please give it a ๐, ๐ฆ, or ๐ and consider:
- signing up for my free weekly dev newsletter
- subscribing to my free YouTube dev channel
What is Object Mutation?
As a quick refresher, object mutation is when we change a property on an object or array. This is very different from reassignment, in which we point a different object reference altogether. Here are a couple examples of mutation vs. reassignment:
// Mutation
const person = { name: "Bo" };
person.name = "Jack";
// Reassignment
let pet = { name: "Daffodil", type: "dog" };
pet = { name: "Whiskers", type: "cat" };
And we have to keep in mind this applies to arrays as well:
// Mutation
const people = ["Jack", "Jill", "Bob", "Jane"];
people[1] = "Beverly";
// Reassignment
let pets = ["Daffodil", "Whiskers", "Ladybird"];
pets = ["Mousse", "Biscuit"];
An Example of Unintended Consequences of Object Mutation
Now that we have an idea of what mutation is, how can mutation have unintended consequences? Let's look at the following example.
const person = { name: "Bo" };
const otherPerson = person;
otherPerson.name = "Finn";
console.log(person);
// { name: "Finn" }
Yikes, that's right! Both person
and otherPerson
are referencing the same object, so if we mutate name
on otherPerson
, that change will be reflected when we access person
.
Instead of letting ourselves (and our fellow developers on our project) mutate an object like this, what if we threw an error? That's where our immutable proxy solution comes in.
Our Immutable Proxy Solution
The JavaScript Proxy
object is a handy bit of meta programming we can use. It allows us to wrap an object with custom functionality for things like getters and setters on that object.
For our immutable proxy, let's create a function that takes an object and returns a new proxy for that object. when we try to get
a property on that object, we check if that property is an object itself. If so, then, in recursive fashion, we return that property wrapped in an immutable proxy. Otherwise, we just return the property.
When we try to set
the proxied object's value, simple throw an error letting the user know they can't set
a property on this object.
Here's our immutable proxy function in action:
const person = {
name: "Bo",
animals: [{ type: "dog", name: "Daffodil" }],
};
const immutable = (obj) =>
new Proxy(obj, {
get(target, prop) {
return typeof target[prop] === "object"
? immutable(target[prop])
: target[prop];
},
set() {
throw new Error("Immutable!");
},
});
const immutablePerson = immutable(person);
const immutableDog = immutablePerson.animals[0];
immutableDog.type = "cat";
// Error: Immutable!
And there we have it: we're unable to mutate a property on an immutable object!
Should I Use This In Production
No, probably not. This kind of exercise is awesome academically, but there are all sorts of awesome, robust, and well-tested solutions out there that do the same thing (e.g., ImmutableJS and ImmerJS). I recommend checking out these awesome libraries if you're looking to include immutable data structures in your app!
If you enjoy this post, please give it a ๐, ๐ฆ, or ๐ and consider:
- signing up for my free weekly dev newsletter
- subscribing to my free YouTube dev channel
Top comments (2)
Nice. Did not know
Proxy
object. Is this approach equal toObject.freeze()
?Object.freeze is shallow, so itโll only make first level props immutable. Anything deeper would need to be recursively frozen.