DEV Community

Cover image for Mutation isn't always bad in JavaScript
Nick Scialli (he/him)
Nick Scialli (he/him)

Posted on • Originally published at typeofnan.dev

Mutation isn't always bad in JavaScript

We humans like dealing in absolutes. It's easy. Nuance is hard. Unfortunately for us, everything involves nuance. That's why we should question ourselves if we start to wonder if mutation is always bad.


Hey all, if you enjoy this article I’d love if you signed up for my free, weekly newsletter. 1,500+ other developers have already signed up and are leveling up their web dev skills with me!


The truth is mutation isn't always bad, nor is it usually bad. It just is. It's an excellent tool some languages give us to manipulate objects. Like with any tool, it's our responsibility to use it correctly.

What is object mutation?

Here's a quick refresher on object mutation. Let's say we have a person object:

const person = { name: 'Jarvis', age: 32 };
Enter fullscreen mode Exit fullscreen mode

If we were to change this person's age, we will have mutated the object:

person.age = 33;
Enter fullscreen mode Exit fullscreen mode

This seems innocuous, right?

Where mutation goes awry

Programming is all about communication and expectations. Mutation goes awry when the intent of an operation isn't clearly communicated and when a developer's (or machine's) expectations are violated.

Let's consider the following (bad) use of mutation:

function copyPerson(person, newName, newAge) {
  const newPerson = person;
  newPerson.name = newName;
  newPerson.age = newAge;
  return newPerson;
}
Enter fullscreen mode Exit fullscreen mode

Why is this bad? Well let's look at what happens when we use this function in the wild:

const jarvis = { name: 'Jarvis', age: 32, arms: 2, legs: 2 };
const stanley = copyPerson(jarvis, 'Stanley', 27);

console.log(stanley);
// { age: 27, arms: 2, legs: 2, name: "Stanley" }

console.log(jarvis);
// { age: 27, arms: 2, legs: 2, name: "Stanley" }
Enter fullscreen mode Exit fullscreen mode

Out expectations have been thoroughly violated!

In our copyPerson function, we accidentally assigned newPerson a reference to the same person object. Since they reference the same object, mutating newPerson also mutates person.

How do we fix this? We can do it entirely without mutation by copying the person object using the spread operator and simultaneously overwriting the name and age properties:

function copyPerson(person, newName, newAge) {
  const newPerson = {
    ...person,
    name: newName,
    age: newAge,
  };
  return newPerson;
}
Enter fullscreen mode Exit fullscreen mode

And that will work! But we can also make it work with mutation, and this is totally fine. Some might even find it more readable!

function copyPerson(person, newName, newAge) {
  const newPerson = { ...person };
  newPerson.name = newName;
  newPerson.age = newAge;
  return newPerson;
}
Enter fullscreen mode Exit fullscreen mode

So wait, if this is fine, was mutation actually the culprit? No, it wasn't. It was our lack of understanding about how references work.

Mutability and popular front-end frameworks

Popular front-end frameworks like React use references for render logic. Let's consider the following example:

function App() {
  const [person, setPerson] = useState({ name: 'Jarvis', age: 32 });

  return <PersonCard person={person} />;
}
Enter fullscreen mode Exit fullscreen mode

In this example, the PersonCard component will re-render if person changes.

Actually, let's be more careful in our wording here: the PersonCard component will re-render person references a new object. Again, we can get ourselves in trouble if we mutate person rather than creating a new object.

For this reason, the following code will be buggy:

function App() {
  const [person, setPerson] = useState({ name: 'Jarvis', age: 32 });

  function incrementAge() {
    person.age++;
    setPerson(person);
  }

  return (
    <>
      <PersonCard person={person} />
      <button onClick={incrementAge}>Have a birthday</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

If we click the "Have a birthday" button, we increment the age property of the person object and then try to set the person state to that object. The problem is that it's not a new object, it's the same person object as the prevous render! React's diffing algorithm sees no change to the person reference and doesn't re-render the PersonCard.

How do we fix this? You guessed it: we just have to make sure we create a new object based on person. Then, we can either accomplish the task by mutating the new object or some other means:

function App() {
  const [person, setPerson] = useState({ name: 'Jarvis', age: 32 });

  function incrementAge() {
    const newPerson = { ...person };
    newPerson.age++;
    setPerson(newPerson);
  }

  return (
    <>
      <PersonCard person={person} />
      <button onClick={incrementAge}>Have a birthday</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

If your instinct here is that mutating newPerson is bad because we're using React, make sure to check your assumptions! There's nothing wrong here: newPerson is a variable scoped to the incrementAge function. We're not mutating something React is tracking, and therefore the fact that we're "in React" doesn't come into play here.

Again, it's very important to recognize here that mutation isn't bad. Our misunderstanding of object references and the React diffing algorithm are what caused the buggy behavior here.

When is mutation good?

Now that I have discussed some scenarios in which mutation often gets blamed for buggy behavior, let's talk about when mutation really shines.

Clarity

Often, I find mutation to be clearer. One example I like to use is if we need to create a new array with one of the elements in the array updated. When working in React, I have often seen the following:

function updateItem(index, newValue) {
  const newItems = items.map((el, i) => {
    if (i === index) {
      return newValue;
    }
    return el;
  });
  setItems(newItems);
}
Enter fullscreen mode Exit fullscreen mode

And this works fine, but it's kind of confusing and probably a bit challenging to read for someone who isn't fluent in JavaScript array methods.

A more readable alternative, in my opinion, is to simply create a copy of the initial array and then mutate the appropriate index of the copied array:

function updateItem(index, newValue) {
  const newItems = [...items];
  newItems[index] = newValue;
  setItems(newItems);
}
Enter fullscreen mode Exit fullscreen mode

I think that's a lot clearer.

Working with complex structures

One of my favorite examples of where mutability shines is building a tree structure. You can do this in O(n) time all thanks to references and mutation.

Consider the following array thay represents a flattened tree:

const data = [
  { id: 56, parentId: 62 },
  { id: 81, parentId: 80 },
  { id: 74, parentId: null },
  { id: 76, parentId: 80 },
  { id: 63, parentId: 62 },
  { id: 80, parentId: 86 },
  { id: 87, parentId: 86 },
  { id: 62, parentId: 74 },
  { id: 86, parentId: 74 },
];
Enter fullscreen mode Exit fullscreen mode

Each node has an id and then the id of its parent node (parentId). Our code to build a tree can be as follows:

// Get array location of each ID
const idMapping = data.reduce((acc, el, i) => {
  acc[el.id] = i;
  return acc;
}, {});

let root;
data.forEach((el) => {
  // Handle the root element
  if (el.parentId === null) {
    root = el;
    return;
  }
  // Use our mapping to locate the parent element in our data array
  const parentEl = data[idMapping[el.parentId]];
  // Add our current el to its parent's `children` array
  parentEl.children = [...(parentEl.children || []), el];
});
Enter fullscreen mode Exit fullscreen mode

How this works is we first loop through the data array once to create a mapping of where each element is in the array. Then, we do another pass through the data array and, for each element, we use the mapping to locate its parent in the array. Finally, we mutate the parent's children property to add the current element to it.

If we console.log(root), we end up with the full tree:

{
  id: 74,
  parentId: null,
  children: [
    {
      id: 62,
      parentId: 74,
      children: [{ id: 56, parentId: 62 }, { id: 63, parentId: 62 }],
    },
    {
      id: 86,
      parentId: 74,
      children: [
        {
          id: 80,
          parentId: 86,
          children: [{ id: 81, parentId: 80 }, { id: 76, parentId: 80 }],
        },
        { id: 87, parentId: 86 },
      ],
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

That's really nifty and rather challenging to accomplish without mutation.

Key takeaways about object mutation

Over time, I have come to realize that there are a few key points to understand with respect to mutation:

  • Often we blame mutation for our own lack of understanding about how references work.
  • Popular front-end frameworks like React rely on comparing object references for render logic. Mutating older versions of state causes all sorts of headaches and hard-to-understand bugs. Instead of recognizing the nuance, developers will often avoid mutation entirely anywhere within React code.
  • Mutation is an excellent tool when its usage is clearly communicated.
  • Mutation is an excellent tool if localized (e.g., the mutated object never escapes a function).

Discussion (21)

Collapse
lukeshiru profile image
LUKESHIRU • Edited on

Disclaimer that I have a strong opinion against mutation, but still I get that you might need it sometimes (you can use libraries like immer and you have "the best of both worlds"). That being said...

We can also make it work with mutation, and this is totally fine...

Is it tho? I mean making a copy just to change two values by direct access seems more like "extra work", and I don't know how much "readable" that is:

const updatePerson =
    ({ age, name }) =>
    person => ({
        ...person,
        age,
        name,
    });

// vs...

const updatePerson =
    ({ age, name }) =>
    person => {
        const personCopy = { ...person };
        personCopy.age = age;
        personCopy.name = name;

        return personCopy;
    };
Enter fullscreen mode Exit fullscreen mode

I used currying of course, but even if I didn't, the second one is unnecessarily more complex, and the first one can be simplified even further, and even made more generic:

const updateObject = update => object => ({
    ...object,
    ...update,
});
Enter fullscreen mode Exit fullscreen mode

Defensive copy is just immutability with extra steps from my point of view.

Mutability and popular front-end frameworks

You can just:

const incrementAge = () => setPerson({ ...person, age: person.age + 1 });
Enter fullscreen mode Exit fullscreen mode

You don't need mutation to update a property of an object (ideally you should avoid objects in React's state, if you need multiple values you can have multiple states).

When is mutation good? - Clarity

const updateItem = newValue => index =>
    setItems([...items.slice(0, index), newValue, ...items.slice(index + 1)]);
Enter fullscreen mode Exit fullscreen mode

No need to map to update a single value, and if you want to do items[index] = newValue you can just use immer and keep it immutable, but either way you do that once and then just use it as updateItem.

Working with complex structures

You could also avoid mutation like this:

const flatToTree = (flat, id = null) => {
    const tree = flat
        .filter(({ parentId }) => parentId === id)
        .map(child => {
            const children = flatToTree(flat, child.id);

            return {
                ...child,
                children: children.length ? children : undefined,
            };
        });

    return id === null ? tree?.[0] : tree;
};
Enter fullscreen mode Exit fullscreen mode

Cheers!

Collapse
jwp profile image
John Peters

Object reuse as a pattern of shape for mutation is just ridiculous. It violates SRP and is always by reference. Thus the birth on non mutable to me was based on malpractice all along.

When a framework dislikes creation of new objects and instead tries to copy and change, it has a fundamental issue in allowing it to happen without warning. Newbies fall on this trap often.

Collapse
lukeshiru profile image
LUKESHIRU

You mean that from your point of view is better to observe changes in every object and throw warnings to the dev when those are mutated instead of just avoiding mutations? Do I need to tell you why is that bad and there are still ways around it? The problem is not with "frameworks" or "libs", is with the way JS behaves regarding objects.

Thread Thread
jwp profile image
John Peters

No what I meant was 25 years of programming experience with no issues with mutating objects speaks volumes.

All the non mutable stuff has only 1 great attribute; which is simply a comparison of two references to instantly detect change.

This same thing has been possible for more than 25 years now in .Net

Thread Thread
lukeshiru profile image
LUKESHIRU • Edited on

I wonder how many years of experience you have 🤣 (*) ... now, what do you think about languages like Haskell with 31 years out there without having mutations? Being that you put so much emphasis in time :/

And if you really think that the only advantage of immutability is referential equality, you might have to do some googling...

* saying "25 years" once was enough, but I guess you really needed to say that.

Thread Thread
jwp profile image
John Peters • Edited on

Ok list some advantages to immutabilty.

As for Haskell is it in top 5 or 10?

Thread Thread
lukeshiru profile image
LUKESHIRU
Thread Thread
jwp profile image
John Peters • Edited on

The 7 articles shown are not absolute truths but are presented that way. Total hype.

Thread Thread
lukeshiru profile image
LUKESHIRU

Whatever dude, whatever. Peace out. God Bless

Collapse
andreidascalu profile image
Andrei Dascalu • Edited on

This kind of thinking is somewhat flawed.

  1. Mutation doesn't get blamed due to anyone's lack of understanding about references. it's about expectation pure and simple. If I have an object x and it's still named x then it shouldn't change. If it changes, its name should reflect it. It's not that looking at the code wouldn't tell me about the change, it's that it requires revisiting the code over and over whenever I need to answer a question about state instead of me following a simple convention that a variable name is descriptive enough.
  2. The idea that mutation is "excellent" if the mutation doesn't leave a function is just wrong. It might make it acceptable but the fact that it doesn't leave a function isn't some benefit it adds, it merely doesn't permeate side effects. That's not a value in itself.
  3. The notion that using mutating language constructs make code readable is actually one of the reasons I laugh at the concept that a language is just a tool. It's a tool alright but one that forms habits and mentality. The fact that a developer can get along with mutating language constructs because Ina certain language it provides readability (despite its downsides) is something that will carry over. It becomes a way of doing things and everyone hangs on to their ways during any transition. This particular one is problematic because it's related to the particular syntax sugar of JS which teaches developers that mutation is acceptable, but you won't fare well if you take this habit elsewhere.
Collapse
jwp profile image
John Peters

Objects shouldn't change without a name change? You mean like an object of me and my age or address should never change? Hasn't ever been an issue in C#, ever.

Collapse
andreidascalu profile image
Andrei Dascalu

Objects shouldn't change in a context where the local logic doesn't expect it to. That's the gist.
In C# you are forced to do conscious decisions about mutability so certain behaviours are expected. Rust falls in the same place.

Thread Thread
jwp profile image
John Peters • Edited on

Yes this is exactly why mutation is never an issue in C# because the context is fully known. The same applies to Javascript. "Know thy context". Trouble happens in Javascript due to bad practices. Immutabilty works perfectly in proper context but is not a "single tool solves all" where frameworks require it.

Thread Thread
andreidascalu profile image
Andrei Dascalu

Sorry but it's not at all the same thing. JavaScript doesn't provide immutability mechanisms at all. In C# you can specify "readonly" on object property and go to bed. In Rust you actually need to allow mutation specifically with "mut". In JavaScript you need to re-investigate the context, as anyone might inadvrtedly call the wrong function on the wrong thing. Better leave a whole lot of documentation on which functions use mutating constructs and end up mutating stuff themselves and hope everyone does flawless code review.

Thread Thread
jwp profile image
John Peters

You missed my point which ultimately was that the immutable fan club became that way due to Javascript programmers not knowing context. It is a 10 pound hammer for a 1 pound problem. So much so the magical spread operators were invented that allow mutation and return a new array. Over engineering in my opinion.

Collapse
jwp profile image
John Peters

I attribute the 'don't mutate gospel' to inexperience. It was accepted at face value in Js community solely because of opinionated frameworks like React.
So much so, that we now see entire component designs requiring its use.

In a poll here on Dev.to, I asked for comments on the goodness on non mutable design. The best answer was 'to avoid per element comparisons' to determine what changed in an array'. All other answers were weak.

We got by just fine mutating objects in C# for 30 years. So the new found non mutable crowd has little history to stand on.

Also JS didn't really have strong typing nor did users really understand by reference and by value. All contributed to traps.

It's all about perspective and experience. Either way works, but what I choose in the end is simply the right thing for my project.

This article is greatl!

Collapse
nas5w profile image
Nick Scialli (he/him) Author

I really appreciate this perspective, John. As I mentioned in the introduction, we often find ourselves clinging to dogmatic views based on some bad experiences or simply because nuance is hard.

I also think the “opinionated frameworks like React” part is more like a misunderstanding a lot of folks have about React. The truth is React works based on referential equality, so its only opinion on mutation is that you shouldn’t mutate an object if you expect React to detect that change and take an action based off of it.

Anyways, thank you so much for the thoughtful response!

Collapse
jwp profile image
John Peters

My thoughts on React being opinionated is based on how it demands its own way of doing things. For example just look at how it did styling. React is 20 times less opinionated than Angular which is good. The advent of webassembly brings all discussions like these to an end. We now have the ability to write entire websites using c# and it's huge set of class libraries. Cool

Collapse
hareom284 profile image
Zaw Zaw Win

You are explaination is save alot of time in debugging when I work in JS.This artical is freaking cool.

Collapse
nas5w profile image
Nick Scialli (he/him) Author

Thank you, I really appreciate that!

Collapse
therakeshpurohit profile image
Rakesh Purohit

I prefer not to mutate the object in any case.