React, the popular JavaScript library for building user interfaces, provides developers with a powerful toolset for creating interactive and dynamic web applications. One of the key principles in React development is immutability, which plays a crucial role in ensuring efficient rendering, improving performance, and maintaining application state. In this article, we will explore the concept of immutability in React and demonstrate its importance through practical code examples.
Understanding Immutability
Immutability refers to the property of an object that cannot be modified. Once an immutable object is created, its state remains constant throughout its lifetime. Immutability offers several benefits, including simplicity, reliability, and improved concurrency. It allows for safer and more predictable programming since objects cannot be accidentally modified, leading to fewer bugs related to unexpected state changes.
In JavaScript, variables themselves are not inherently immutable or mutable. However, the way you assign values to variables and the nature of those values determine whether they are mutable or immutable.
Les variables qui contiennent des types de données primitifs tels que les nombres, les chaînes de caractères, les booléens, null et undefined sont immuables. Une fois affectées, leurs valeurs ne peuvent pas être modifiées. Si vous essayez de modifier une variable contenant une valeur primitive, cela crée en réalité une nouvelle valeur en mémoire.
let a = 5;
a++
console.log(a); // 6
Objects and arrays: Variables that hold objects (including arrays, functions, and objects created with new Object()) are mutable. Although the variable itself can be reassigned to a different object, the contents of the object can be modified.
// La méthode Array.push() est une méthode mutable car elle
// modifie directement la liste sans avoir besoin de le
// réaffecter à la variable. Son retour est la nouvelle longueur
// de la liste.
const a = [1, 2];
a.push(4); // [1, 2, 4]
// En JavaScript, il est possible d'injecter une nouvelle valeur
// à n'importe quelle position dans un tableau de la même manière
// que vous pouvez ajouter n'importe quel champ dans un objet.
const b = [1, 2, 3];
b[4] = 5; // [1, 2, 3, <1 empty slot>, 5]
In order to achieve immutability with complex objects like objects and arrays, we simply need to make sure to assign a new instance of the data into the variable. For example:
// En utilisant l'opérateur de propagation (spread operator),
// nous pouvons copier le contenu de la variable c dans une
// nouvelle liste et y ajouter ce que nous souhaitons.
let c = [1, 2, 3];
c = [...c, 3];
// Les méthodes telles que slice renvoient une copie superficielle
// (shallow copy) de la liste. Avec cette nouvelle instance de la
// liste, nous pouvons utiliser des fonctions mutables comme
// push() pour manipuler le contenu de la liste sans affecter la
// variable d'origine.
const d = [1, 2, 3];
const updatedD = d.slice();
updatedD.push(4);
// concat() est un autre exemple d'une fonction de liste qui ne
// mute pas (ne modifie pas) la liste d'origine.
const e = [1, 2, 3];
const updatedE = e.concat(4);
Why is it important to use immutability in React?
To understand why immutability is important, we need to understand how React works under the hood. In order to be efficient in its rendering, React is composed of a Virtual DOM (Document Object Model) that is a simplified, lightweight representation of the actual DOM. React uses it to efficiently update and render components. It is an abstraction layer between the component structure and the browser's native DOM.
The lifecycle of a React component goes in 4 steps:
- Component render: When a React component is rendered, it generates a virtual DOM tree, which is a JavaScript object representing the component's structure, properties, and children.
- Diffing: When the component's state or props change, React creates a new virtual DOM tree for the updated component and performs a "diffing" process, where it compares the new virtual DOM tree with the previous one to identify the minimal set of changes required to update the actual DOM.
- Virtual DOM reconciliation: React efficiently updates only the parts of the actual DOM that have changed, minimizing unnecessary re-rendering. It computes the difference between the new and old virtual DOM trees and applies only those changes to the real DOM.
- Rendering: Finally, React applies the calculated changes to the real DOM, resulting in an updated user interface.
The important part for us is the second step. The only thing that makes a component update itself is a change in props or a state. Let’s take a look at some basic examples of state management implemented in different ways.
Updating the State Value directly
const IncrementButton = () => {
let [value, setValue] = useState<number>(0);
return (
<button onClick={() => value = value + 1}>
{value}
</button>
);
};
In order to update the UI to increment the value, we need to trigger the lifecycle of React. By doing so, React is able to update the virtual DOM and works its magic. When updating a state, it is required to call the set state action given by the useState hook, as it is React’s way to trigger the lifecycle. In our example, the update of the variable value won’t trigger the lifecycle and the UI won’t update.
The new value won’t even be persisted upon further update, as a React component is a regular Javascript function that runs on each update. In our example, the variable declaration of “value” would simply be created again from the value stored in the useState. So, we must never reassign the value of a state directly, but use the setValue method instead.
const IncrementButton = () => {
const [value, setValue] = useState<number>(0);
return (
<button onClick={() => setValue(value + 1)}>
{value}
</button>
);
};
Mutate the state before updating it
const NumberListing = () => {
const [numbers, setNumbers] = useState<number[]>([]);
const addNumberToList = () => {
numbers.push(numbers.length + 1);
setNumbers(numbers);
};
return (
<>
<button onClick={addNumberToList}>Add Number</button>
<span>Numbers:{numbers.join(', ')}</span>
</>
);
};
In this example, we have a state containing a list of numbers that we want to expand by the click of a button. In order to do so, the code uses the “push” function of the array to add an element in it. As mentioned previously, push is a mutable function of the array. Because the object type is an array, the constant numbers are simply a reference of the array defined in the state. Those 2 elements means that using the “push” on the constant updates the internal value of the “useState”. So, where is the problem here? We do call the set state function after it! Why would it be problematic?
The issue with this code is coming from the state management of React. To optimize the rendering, React compares the current state with the newly generated one to see if there is actually a change. By using the mutate function “push”, we accidentally updated the state with the same value of the new, resulting in no difference between the two, thus preventing any useless update. Immutability is primordial in order to inform React the states are going to be differen
In this situation, using an immutable solution, like the spread operator, would fix our rendering issue. The same goes for Javascript objects or classes, as they are handled with reference.
const NumberListing = () => {
const [numbers, setNumbers] = useState<number[]>([]);
const addNumberToList = () => {
setNumbers([...numbers, numbers.length + 1]);
};
return (
<>
<button onClick={addNumberToList}>Add Number</button>
<span>Numbers:{numbers.join(', ')}</span>
</>
);
};
Conclusion
Having issues with mutable values in React is a basic step in every frontend developer using the library. Like many libraries and frameworks, understanding how React works under the hood can empower you to develop the best product in the simplest way.
When updating a state:
1. Make sure to use the update function to trigger the lifecycle of React.
2. Make sure the new state is different from the old one. It is simple with primitive types, but it requires a bit more imagination for mutable types, such as arrays and objects.
Les articles en vedette
How to Justify a Refactor?
Metaphors in Software Development: Understanding a Complex Universe
An Innovative Tool to Support Software Development Teams
Soyez les premiers au courant des derniers articles publiés
Abonnez-vous à l’infolettre pour ne jamais rater une nouvelle publication de notre blogue et toutes nos nouvelles.