Why is immutability so important in React?

30 March 2020

This article is for people who have some grasp of JavaScript and are working with React. It doesn’t only include junior developers beginning to sail in a lake of props and state, but also applies to senior developers who started a long time ago, who are perfect sailors but they haven’t got time to step onto dry land and check out new boats.

As you would deduce from the title, this article is going to be about immutability and React. In general it is a very vast topic and I don’t want to dive into fundamentals, because it would require another article (or rather series of articles). If you are really interested in this topic the Internet is full of material to begin with. I highly recommend adopting such ideas as pure functions, the single responsibility principle and in general functional programming.

For our purposes, we will focus directly on how to achieve immutability in React. But first what is it? Oxford Dictionary provides us with such a definition of immutable:

“Unchanging over time or unable to be changed.”

Translating to JavaScript, it’s pretty much the same.

Why is immutability so important in React?

No, but wait. I can do better. A pretty good explanation of what immutable is and what the opposite is when you assign a string value to const and let. The former cannot be changed and the latter can. Another example: you are building a system for ordering sandwiches and you have just created the variable “sandwichName”. This name is pretty specific. Whenever this variable pops up anywhere in the codebase, you will think that this is probably a string representing the name of the sandwich ordered and not a boolean or a response from an API containing a list of Chuck Norris jokes. But somewhere along the way something bad happens and your “sandwichName” becomes the current date. Well, this means that your ordering system may benefit from an immutable approach.

Now, after this introduction let’s check how immutability is related to React!

All examples listed below can be found in the Github repository and CodeSandbox I created especially for this article. If something is unclear, please look into the code and check for yourself.

Storytime: you got a ticket to add a new feature – the user clicks on a button and a new item is added to the list of numbers. Easy peasy, the list can be kept inside a component’s state and you think of yourself as a traditionalist, so you create a class component (calm down, the function component will be discussed later). Everything goes well, the state is initiated, the list is rendered, your styling is perfect. All that remains is to add an onClick function for the button, so you create:

addItem = () => {
   this.setState(prevState => {
     const newItems = prevState.items;
     const lastItem = newItems[newItems.length - 1] || 0;

     newItems.push(lastItem + 1);

     return { items: newItems };
   });
 };

 

You are proud of your work. Your function uses API that guarantees the latest state, you create a temporary copy of items, then you find the value of the last element, add a new incremented value to a temporary copy and finally set it as a new state. And it works! Good job!

Or does it? Somewhere in the code review your colleague points out that your component should be PureComponent, because its parent often rerenders and your particular component doesn’t have to. But when you change it to PureComponent your function suddenly stops working. Your first thought might be that PureComponent is buggy and jQuery is far better than some fancy React stuff. But hold on! There is a pretty good explanation of what is happening.

Read also: Guava cache vs Caffeine

First of all, we have to understand the difference between Component and PureComponent. Component is rerendered every time its parent is rerendered, its props have changed or a setState is called. PureComponent is rerendered every time props have changed or the state has changed. The takeaway here is that PureComponent has a built-in comparison of props and state that is triggered whenever the parent rerenders or a setState is called.

Now, let’s look at the addItem function above, especially at this one little boy:

const newItems = prevState.items;

 

I wrote earlier that this line creates a temporary copy of items in state. That was a lie. This line assigns a new constant that is a reference to the state’s property items. In our case this property is an array – compound type. In JavaScript there are two groups of variables:

– scalar primitive values (Number, String, Boolean, undefined, null, Symbol)

– compound values (Object, Array)

Scalar values are immutable and compound values are mutable. So, if we change newItems, we also change items in state without setState! And that is a terrible mistake. Unfortunately, we are changing newItems by using the push function to add the new item to the list. Then, we are returning the “new” state, a built-in comparison function is called and it turns out that there is no difference between the “new” and “old” items, because we previously overwrote the “old” items with “new” ones.

To check for yourself that what I’m preaching makes sense, please add the new functionality – string text in state with some description and length of items. Do something like this in the addItem function:

let newText = prevState.text;
newText = `I will always work!. There are ${newItems.length} items.`;

return { items: newItems, text: newText };

 

As you may deduce from my confidence, this will work perfectly, because newText is created with the value of prevState.text, not its reference. prevState.text is a string – scalar primitive value – so it is immutable.

How can we guard ourselves from such mistakes? First of all, we could create a real copy of the array using slice or ES6 spread operator:

const newItems = prevState.items.slice();
const newItems = [...prevState.items];

I am big fan of ES6, so I would write an addItem like this:

addItem = () => {
   this.setState(({ items: prevItems }) => {
     const lastItem = prevItems[prevItems.length - 1] || 0;

     return {
       items: [...prevItems, lastItem + 1]
     };
   });
 };

Why is immutability so important in React?

And now, for those who waited for an example with a function component and hooks! Let’s rewrite our component so it uses a useState hook. The refactored addItem function looks something like this:

const addItem = () => {
   setItems(prevItems => {
     const newItems = prevItems;
     const lastItem = newItems[newItems.length - 1] || 0;

     newItems.push(lastItem + 1);
     return newItems;
   });
 };

I hope you won’t be surprised when I say that it still doesn’t work, because of the same reasons I have written above. The one that will work looks like this:

const addItem = () => {
   setItems(prevItems => {
     const lastItem = prevItems[prevItems.length - 1] || 0;
     return [...prevItems, lastItem + 1];
   });
 };

All in all, if you want to be a knowledgeable sailor in the React Sea, immutability must be your very best friend. The principle of operation described works everywhere in the React world. In the Github repository you can see analogical examples that are using render props to pass values between components. Of course, those that are assigning references to props objects don’t work and those that are using an immutable approach work perfectly. In my opinion there is no need to install immutability helpers to keep an immutable state inside React components. I highly recommend getting to know such ES6 features as the map, filter, reduce functions and the spread operator (in arrays and objects), because using these is definitely seen as best practice to achieve immutable design in React app. These tools help a lot on the treacherous waters of props and state.

Read also: How to Develop an App: Understanding the Development Process

Cookies Policy

Our website uses cookies. You can change the rules for their use or block cookies in the settings of your browser. More information can be found in the Privacy Policy. By continuing to use the website, you agree to the use of cookies.
https://www.efigence.com/privacy-policy/