The immutability (unchangeability/staticity,) in JS and React
Or why it's important in state management 😊
Hello everyone, our current article explores topics that may seem basic, but sometimes even veteran developers can break a tooth in them (pl.: e.g., due to lack of sleep from the kids or from playing Baldur’s Gate III or Helldivers II).
We'll overview what immutability means in general, how it surfaces in state management and how can we use it.
By the end of the article, hopefully, you'll become a true master of the topic.
What is immutability?
Immutability is a software development concept that refers to certain data or objects not changing after their creation. In other words, immutability is a property where data or objects are created once and then do not change.
How does immutability appear in JavaScript, where can I encounter it?
Immediately, JavaScript has immutable types (primitives). JavaScript doesn't even have types, one could say, but we couldn't be more wrong!
Despite being a dynamically typed language, JavaScript does have types, and variables don't need to specify what will be placed in them (dy-na-mic).
The typeof operator reveals to us the type of each value. In the case of JavaScript primitives, when we want to access a property, auto-boxing occurs, wrapping them into the corresponding wrapper class so that their characteristic properties work on them.
Let’s take a look!
var str = "asd"
// str.toUpperCase() already works thanks for auto-boxing
str[1] = "h"
console.log(str) // "asd"
Even if we attempt to change its value, it's unsuccessful; the original string remains unchanged.
How can data mutation occur then?
Let's start with my little one described as an object below.
const baby = {
ageInMonth: 12,
weight: 10,
name: "Alex"
}
However, my grandma calls the baby "Haultsy" (total misunderstanding, doesn't deserve a word). In code:
const grandChild = baby
grandChild.name = "Haultsy"
console.log(grandChild)
When my wife comes home, she finds that the child's name has changed.
console.log(baby.name) // "Haultsy"
And thus begins her bewilderment (even though she's otherwise very smart and beautiful and everything else ^^). Yet, all that happened, is that in JS, passing by values in the case of non-primitives happens by reference and unintentionally we mutated the baby’s name. You can’t recognize him anymore 😊.
Based on this, we can move on to the next stage of our article, discussing what happens when mutating React state.
How can we prevent mutation?
- By using the JSON parse and stringify methods
As mentioned above, JavaScript primitives are immutable. So, a logical choice would be to first convert the object into a string using JSON.stringify, then parse it back, and store the result in a variable.
In our case, it would look like this:
const grandChild = JSON.parse(JSON.stringify(baby))
grandChild.name = "Haultsy"
console.log(baby.name)
We get "Alex", as we should.
- Using Object.assign method
To avoid handling the object by reference, in both cases two and three, we create a new object, store it in a variable, and then modify it.
const grandChild = Object.assign({}, baby)
grandChild.name = "Haultsy"
console.log(baby.name)
We get "Alex" as we should.
Object.assign it copies the second parameter into the empty object created as the first parameter, thus achieving the desired effect: creating a new object and only modifying its "name" property.
- Using spread operator
The effect described in point two can also be achieved using the spread operator as well.
const grandChild = {...baby}
grandChild.name = "Haultsy"
console.log(baby.name)
Again the result is "Alex".
We create a new object and copy the contents of the baby object into it.
Warning:
There's a slight issue with how object copying was done in points two and three. While it's perfect for our current example, we need to be aware that this only creates a shallow copy of the given object. Therefore, it's unable to properly clone deeply nested objects, so the process only occurs to a depth of one level.
And here comes the next two points.
- Using structured clone
With the still relatively new global function structuredClone() it's easy to address this problem by creating a deep clone of the given object.
Let’s see in action:
const grandChild = structuredClone(baby)
grandChild.name = "Haultsy"
console.log(baby.name)
Again we get "Alex".
Now, with the data nested in multiple levels, everything is fine. There's no longer a reference between them; a new object instance has been created.
- Using immutability lib
If it's feasible to slightly increase the size of our JavaScript package and our team members are willing to learn how to use it, using an immutability library (like) could be a helpful solution.
With its usage, such problems of this nature can be eliminated.
Immutability Issues in React State Management: Practical Example
I believe a practical example will be most helpful rather than outlining it in a novel-like form. Let's present the problem with the following code.
import {useState} from "react"
const initialState = [
{
id: 1,
name: "John",
hobbies: [
{name: "football", id: 60, active: true},
{name: "baseball", id: 61, active: true},
{name: "basketball", id: 62, active: false},
{name: "tennis", id: 63, active: true}
],
friends: [2, 3, 4],
},
{
id: 2,
name: "Jane",
hobbies: [
{name: "knitting", id: 51, active: true},
{name: "sewing", id: 52, active: true},
{name: "reading", id: 53, active: true},
],
friends: [1],
}
]
const Test = () => {
const [data, setData] = useState(initialState)
const handleCeaseFootball = () => {
setData((prev) => prev.map((person) => {
person.hobbies.forEach((hobby) => {
if (hobby.id === 60) {
hobby.active = false
}
})
return person
}))
}
return (
<div className="flex p-40 w-full justify-between">
<pre className="bg-blue-200 p-20">{JSON.stringify(data, null, 1)}</pre>
<div className="flex gap-8 h-full">
<button
className="p-2 bg-amber-100"
onClick={() => setData((prev) => [...prev, {
id: 3,
name: "Andrew",
hobbies: [
{name: "football", id: 60, active: true},
{name: "sword fighting", id: 64, active: true},
{name: "basketball", id: 62, active: false},
{name: "video gaming", id: 65, active: true},
],
friends: [1, 2, 4],
}])}>
Add Andrew friend
</button>
<button
className="p-2 bg-amber-100"
onClick={handleCeaseFootball}>
cease football
</button>
<button
className="p-2 bg-amber-100"
onClick={() => setData(initialState)}>
reset state
</button>
</div>
</div>
)
}
export default Test
Reviewing the Code
Upon reviewing the code, we can see that we initialize the component's state with the content of the initialState variable (an array of objects).
There's a pre tag where we "display" the data, along with three buttons.
- The first button adds a hardcoded person to our stored state.
- The second button sets the football activity of all people to false.
- With the third button, we can reset our state to the content of the initialState variable, effectively returning it to its initial state.
Using and Drawing Conclusions
Firstly, in the "display", we can see that there are two people, and after pressing the first button, there are three in the state. If we inspect closely, we notice that two people (Andrew and John) have football as their hobby.
Then, we click the second ("cease football") button. As expected, we see in the "display" that the activity of those people whose hobby is football has changed to false.
Finally, we try to reset our state to the default with the "reset state" button, but it doesn't happen as expected. The initial state is not restored.
Fixing it
It's no secret that the problem occurred in the handleCeaseFootball function, precisely when setting the football hobby activity to false (hobby.active = false). With this action, we mutated the hobbies array.
As mentioned earlier, there are several solutions, and here we present one using the spread operator.
const handleCeaseFootball = () => {
setData((prev) => prev.map((person) => ({
...person,
hobbies: person.hobbies.map((hobby) => {
if (hobby.id === 60) {
return {
...hobby,
active: false
}
}
return hobby
})
})))
}
We make a shallow copy of both the people and their hobbies. With this, we have achieved the correct functioning of the code without any data mutation.
Conclusion
In summary, the article thoroughly discussed the concept of immutability and its paramount importance in JavaScript and React development. Using practical examples, we illustrated potential problems and their possible solutions. We examined in detail how to avoid pitfalls in React state management and how to handle them correctly.
We hope that the information provided in the article has helped you better understand the significance of immutability and its practical application. We trust that the examples and tips presented here will assist you in effectively managing React states, thereby enhancing code stability and readability.