DEV Community

loading...
Cover image for Don't stop Mutating

Don't stop Mutating

Stephan Meijer
Fullstack React, Typescript, MongoDB developer | maker of rake.red, updrafts.app, testing-playground.com, issupported.com, leaflet-geosearch
Updated on ・4 min read

I recently came across a tweet by Simon Høiberg that basically forbids you to use delete. The reason for this? "You don't want to mutate the existing object. It leads to inconsistent and unpredictable behavior"

This kind of advice turns me sad. I mean, the keyword is there. JavaScript allows us to delete properties from an object. Why not use it?

Don't get me wrong. There is a truth hidden in Simon's message. There are scenarios where you can easily avoid that keyword. And there are scenarios where mutating will cause trouble. The question is, do you really need to avoid it like the plague?

Immutable or Readable

The internet speaks about two primary reasons why you shouldn't be using delete.

  1. Mutable, delete mutates an object which is bad. 1
  2. Performance, delete has serious performance impact. 2 3

Readability doesn't seem to be very important nowadays. So let me focus on that part.

But first, let's take a look at some code. I find it easier to talk that way. I've taken Simon's example. We have a number of users, and want to delete the age property.

const users = await fetchUsers(100);
const newUsers = [];

for (let i = 0; i < users.length; i++) {
  const { age, ...newUser } = users[i];
  newUsers.push(newUser);
}
Enter fullscreen mode Exit fullscreen mode

How was that? It's a quite basic snippet, so I hope it was easy to understand. The above, is the version that uses object destructuring and also pushes the users without the age to a new array. Because, if we don't want to mutate the user records, we also don't want to mutate the list. It wouldn't make much sense otherwise.

Now, please compare it to the next example, where I don't know any better, and simply mutate the data.

const users = await fetchUsers(100);

for (let i = 0; i < users.length; i++) {
  delete users[i].age;
}
Enter fullscreen mode Exit fullscreen mode

How was that for readability? I definitely prefer the last one. It's way easier to see what's going on. Sure, I understand the first one perfectly fine. That's not what this is about. The mutating variant simply adds less noise.

Unpredictable behavior

I can hear you think. But what about the "unpredictable behavior"?!. One example that I instantly can come up with where mutating can cause trouble, is in React. React uses mutations to detect when it should update the user interface (DOM). So yes, it's important there.

That being said, if you fetch a large object from a rest api, and wish to do some cleaning before you save the object in a state/store. Than why could it not be a mutating action?

Basically, if we take the example from above, and would wrap it in a function. What trouble can it give us?

async function getUsersWithoutProjects() {
  const users = await fetchUsers(100);

  for (let i = 0; i < users.length; i++) {
    delete users[i].projects;
  }

  return users;
}
Enter fullscreen mode Exit fullscreen mode

Do you have the answer? Right.., none! Because for the outside world, users never had that property to start with. The data is created and mutated in the same boundary (/scope). Because users never left this function with the projects attached, nothing can depend on it.

Performance

But what about performance?!! Well, are you deleting large values or small values? A single one, or thousands? How does the rest of your code perform? If you don't know, then don't worry about it. You can try to optimize till the latest ms, but if the data request takes hundreds of milliseconds, would that delete call really make a difference?

I've created a simple perf.link that shows you that delete doesn't need to be slower than the alternative. It is one case out of thousands of potential scenarios. All I'm saying is, it's not black and white. If you have an edge case, please do what feels best. I'm confident that there are cases where delete is the performance bottleneck. But I'm just as confident that 99% of us, will never work on those kinds of projects.

Then the other thing about performance. Not regarding delete, but regarding mutating. If it's about assigning new values to properties instead of reconstructing entire objects, mutating is seriously faster. Again, in most cases, reconstructing objects and working in an immutable way performs fine. You won't experience any slowness because of it. But in those other cases, mutating data is okay. Maybe even preferable.

Conclusion

I hope you liked this article. Because I'm not going to tell you if you should mutate your objects or not. Both mutable as well as immutable solutions have their time and place. Use them accordingly, and do what feels best. In most cases, go with what's easiest to read.

This article is another attempt of me to stop the "DON'T DO THIS" shouting on the internet. Programming isn't black and white. We can't just ban half the keywords or native functions because they "feel wrong". There is a valid use case for each and every function.


👋 I'm Stephan, and I'm building updrafts.app. If you wish to read more of my unpopular opinions, follow me on Twitter.

Discussion (18)

Collapse
jackmellis profile image
Jack

I think this is quite a dangerous thing to encourage. In your example, fetchUsers could be bringing the data in from anywhere, it could be caching the result, it could be returning a hard-coded value, etc.

If you mutate the resulting users, what if another part of your code is also using it in order to get a list of ages? This would completely break it.

Immutability for the purpose of avoiding bugs is so much more important than readability or performance.

The real readability problem in your examples is that there's no natively elegant way to omit a property. Wouldn't you agree this looks nicer with a basic helper method?

const users = fetchUsers(100).map(user => omit(user, 'age'));
Enter fullscreen mode Exit fullscreen mode
Collapse
smeijer profile image
Stephan Meijer Author

Sure, helpers help. Please understand, my examples are contrived examples. I assume that fetchUsers fetches data from a remote origin. An API call so to say. Should I have mentioned that in the article?

Your comment is valid. But that does not make my article invalid. I also don't want to encourage mutating data. I want to encourage people to see that there is a valid scenario for both ways. I wish people to stopped banning half the language, simply because they:

  • find it hard to grasp
  • think it's prone to bugs
  • think the new way is better

Immutability for the purpose of avoiding bugs is so much more important than readability or performance.

That can definitely be a good argument to make something immutable. And honestly, when working on the frontend, it often is. On the other hand, I'm also working on the backend, with geospatial data (geojson). When I run geospatial operations, such as geometry simplifications, mutating objects is WAY faster. It has a noticeable impact, that we confirmed by profiling real requests. When someone submits a form to my backend, and I just normalize/simplify that data before I send it to the database, it makes zero sense to do it in an immutable way.

My goal with articles like this isn't to encourage anything else than to open the readers eyes. "There is a valid use case for each and every function.". A lot of articles push readers in a specific direction. And (junior?) developers are very sensitive to that and start refactoring stuff right away. Only to come back to it years later.

Collapse
jackmellis profile image
Jack

I 100% agree that the developer community is sometimes too opinionated and that everybody should be doing things a certain way. And there are of course instances where mutability is fine, even preferable, usually when you're writing internal code where you fully understand the implications of mutating data.

Your snippets have a good example of this. You're mutating an array by pushing to it, but you also created that array yourself directly before, so you have confidence that mutation won't cause any bugs.

For me I tend to do immutability-first and then think "what benefit would this have if I made it mutable?"

Collapse
bravemaster619 profile image
bravemaster619

You shouldn't have included projects attribute in the first place.

Anyways, deleting something somewhere in your code will cause unpredictable behavior in the long run.

You better leave original object as it is, because mutating objects WILL affect other parts of your code.

I agree on the second example (Unpredictable behavior) but the first one is kinda misleading. I would say.. Readability vs Maintainability

I will definitely choose this way:

const users = fetchUsers(100);
const newUsers = [];

for (let i = 0; i < users.length; i++) {
  const { age, ...newUser } = users[i];
  newUsers.push(newUser);
}
Enter fullscreen mode Exit fullscreen mode
Collapse
smeijer profile image
Stephan Meijer Author

Yes, in the perfect world, projects wouldn't have been there in the first place. Unfortunately, we don't live in the perfect world. Sometimes we use external API's, and get more data than we need. Sometimes, we fetch data from MongoDB, and deleting a bit of data is easier done on the backend than it is through aggregation piplelines.

You better leave original object as it is, because mutating objects WILL affect other parts of your code.

No, it doesn't have to. If the mutation is abstracted away, there is no issue (my getUsersWithoutProjects example). And that's the point of the article. It really isn't that black and white.

I will definitely choose this way:

And that's okay. Everyone has their own preference. The fact that you choose this style, doesn't make someone choosing the other wrong.

Collapse
bravemaster619 profile image
bravemaster619

Sometimes, we fetch data from MongoDB, and deleting a bit of data is easier done on the backend than it is through aggregation piplelines.

Yeah, maybe it's easier to do in frontend. BUT you need to do in BACKEND. For the sake of security and performance. Right?

If the mutation is abstracted away, there is no issue (my getUsersWithoutProjects example)

I already said that I agree on getUsersWithoutProjects.

The problem lies in this one:

const users = fetchUsers(100);

for (let i = 0; i < users.length; i++) {
  delete users[i].age;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
tbm206 profile image
Taha Ben Masaud

If readability is the main concern of this article, I'd simply wrap the non mutating code in a delete function. Many libraries, such as lodash, provide this.

Collapse
smeijer profile image
Stephan Meijer Author

It's not. The main concern of this article is that too many of us (/other articles) lack nuance, and try to push developers in a single direction. While development isn't that black and white.

The TLDR is in the last paragraph:

Programming isn't black and white. We can't just ban half the keywords or native functions because they "feel wrong". There is a valid use case for each and every function

Collapse
tbm206 profile image
Taha Ben Masaud

That is quite a statement. I'm not aware of any credible advice out there where the reasoning revolves around "feel wrong".

Mutation is definitely bad from my experience. While I understand where you come from, the advice in this post does not offer credible proof that promoting different styles is more important than actually following best practices.

Thread Thread
smeijer profile image
Stephan Meijer Author

Mutation is definitely bad from my experience.

Expressions like that are the reason I wrote this article. Mutating data is not "definitely bad". There are very valid scenarios where you want to avoid immutable patterns. And that's what I tried to explain in this post.

It's unfortunate that I didn't get this message expressed more clearly.

Thread Thread
tbm206 profile image
Taha Ben Masaud

Mutation can only be beneficial because of machine code and computer architecture.

In an ideal world, e.g. higher level language, mutation should be avoided unless the developer really enjoys debugging.

It's clear you're stubbornly convinced that the wider community is at fault for sharing best practices; that I can do little to change.

Thread Thread
smeijer profile image
Stephan Meijer Author

It's clear you're stubbornly convinced that the wider community is at fault for sharing best practices;

Thank you for the kind words.

I like to think that I'm stubbornly convinced that I'm slighly frustrated by the unnuanced "best practices" I find on the web, that are blindly followed as the "only truth" by a bit too many developers.

Collapse
tomburgs profile image
Toms Burgmanis

I agree. The features are there for a reason and everything has its own use cases.

Developers should know why they are doing it the way they are doing it, and not because some random dude on Twitter told them to.

Collapse
smeijer profile image
Stephan Meijer Author

THANK YOU! It's truly nice to see that some people get it.

I'm not here promoting mutability or immutability patterns. I'm here to open eyes and make developers understand that they have to make a choice on a case by case basis.

Collapse
jacobmgevans profile image
Jacob Evans

I really appreciate the "this article isn't telling you what to do" feel you make sure to emphasize while still making a sound argument for your perspective on the issue!

Collapse
smeijer profile image
Stephan Meijer Author

Thanks, Jacob! 😊

Collapse
thorstenhirsch profile image
Thorsten Hirsch

I agree with you in all your examples. But in general I think "don't mutate" is a good advice (it doesn't say "never mutate").

Collapse
smeijer profile image
Stephan Meijer Author

Ooh, I definitely agree. Most of the time, immutable code is fine. Maybe even preferred. I simply try to make a sound against the unnuanced pushing that starts to take overhand on the web. I hope to make especially junior developers realize that they have to make decisions. And that banning half the language because it's "the old way" isn't the solution.