You heard about the term "immutability" in programming.
But why is immutability an important concept? Why is it mentioned so often in programming and especially in the front-end world?
Immutability refers to the concept that you can't change the property of a data structure after its creation. For example, an immutable object remains constant throughout its lifetime.
Immutability as a concept has significant implications in programming languages. Especially in areas of data handling and manipulation.
Some characteristics of the immutability concept:
- Freezy: Once you create an immutable object, you "freeze" its values. You can't change that object anymore. If you want to make the change, you have to create a new object. Then you apply the change to finally overwrite the original object.
- Predictability: Immutable objects don't change state, so they have a predictable behavior. This is especially valuable in concurrent or parallel programming.
- Safety: Immutability contributes to a safer code. With that, I mean eliminating the risk of unexpected changes that can occur in the code. You can reduce bugs caused by modifying shared, which leads to more reliable code.
- Threads friends: Immutable objects are thread-safe. Many threads can access and use immutable objects. This goes without the need for explicit synchronization mechanisms. There's no risk of one thread modifying the object while another is using it.
- Functional as it gets: Immutability is a core principle in functional programming. It encourages the use of pure functions that don't have side effects. It also operates only on their inputs, making code easier to reason about and test.
- Cachy and memoy: Immutable objects are easily cached and reused. That's because their values don't change.
- Undo/Redo friendly: In text editors, immutability simplifies implementing undo and redo functionality. This is achieveable preserving the previous states of objects. Besides text editors, this can be applicable to any application.
Now, let's understand the basics through some code examples.
Mutable data
What's mutable data?
Mutable data is data that you can change without creating a new value. In JavaScript, the best examples are arrays and objects, so let's take a look at this code:
const users = ["didier"]
const newUsers = users
users.push("john")
console.log({users, newUsers});
//prints {users: ["didier", "john"], newUsers: ["didier", "john"]}
console.log(users === newUsers)
//prints true
Even though we added "john" to the users
array, the change is reflected in the newUsers
array also.
Why?
Because in the 2nd line, we made a mistake. If we wanted newUsers
to have all values from users array we should do it like this:
const newUsers = [...users]
Then we wouldn't have this problem.
But since, the code is written like that, both of these arrays point to the same reference. So if you change the data in one array you will change the other one as well.
That's why you get true as a result when you compare users
and newUsers
with shallow comparison. Shallow comparison uses an equality operator.
Any change in the mutable data is not detectable via shallow comparison. The "===" operator can not tell if the content of the array changed.
Immutable data
Immutable data is data that cannot be changed or modified after it is created. So only way to change that data is to create a new value and overwrite it.
In JavaScript, strings are a good example of immutable data:
let player1 = 'didier'
let player2 = player1
player1 = player1.concat(' drogba')
console.log({player1, player2})
// prints {player1: 'didier drogba', player2: 'didier'}
console.log(player1 === player2)
// prints false
If we compare this example to the previous one, we can see that we did the same thing. The difference is that here the value is not reflected on the second variable - player2
after we change the value in player1
.
That's because .concat()
doesn't change the value of the original string. It creates and returns a completely new string.
So we can say that strings, booleans, and numbers in JavaScript are immutable.
Why is immutability important?
You are asking yourself now, ok but why is immutability so important?
Well, it's important because some of the core concepts are based on immutability. Let's take React for example and how re-rending works:
function App() {
const [users, setUsers] = useState([])
const fetchUserData = () => {
fetch("https://jsonplaceholder.typicode.com/users")
.then(response => response.json())
.then(data => {
setUsers(data)
})
}
useEffect(() => {
fetchUserData()
}, [])
if(users.length === 0){
return <div>No users available</div>
}
return (
users.map(user => <User key={user.id} userData={user}/>)
)
}
We know how React's useState hook works, but let's repeat. Whenever the component state changes, React will re-render that component. So React is watching changes in the users variable and then acts accordingly.
To be able to detect changes in the state, React uses shallow comparison between the old and new states. Then it decides if it needs to re-render the component or not.
Now, let's do something stupid in the code:
function App() {
const [users, setUsers] = useState([])
const fetchUserData = () => {
fetch("https://jsonplaceholder.typicode.com/users")
.then(response => response.json())
.then(data => {
users.push(...data)
setUsers(users)
})
}
useEffect(() => {
fetchUserData()
}, [])
if(users.length === 0){
return <div>No users available</div>
}
return (
users.map(user => <User key={user.id} userData={user}/>)
)
}
In the example above, when the data is fetched, we are pushing the data in users
array with the spread operator. Then, we set new data by passing users
variable into setUsers
.
Photo by roman raizen
I know, it's horrible. Your eyes are bleeding but bear with me.
So in the example above, React will not be able to detect any change in the state. Why? Because we are directly modifying the users
array.
As a side-effect of our horrible code, React won't re-render the component when the data is fetched.
Immutability and performance
Before we stated that React uses shallow comparison for detecting component state changes. It's doing that because comparison by value could result in slow performances.
Let's take a look at this example:
function App() {
const [blocks, setBlocks] = useState(new Array(10000).fill({active: false}))
const onClickHandler = (blockIndex) => {
const updatedBlocks = blocks.map((block, index) => {
return index === blockIndex ? {active: !block.active} : block
})
setBlocks(updatedBlocks);
}
return (
blocks.map((block, index) => <button
key={index}
style={{backgroundColor: block.active ? 'green' : 'gray', width: '50px', margin: '4px'}}
onClick={() => onClickHandler(index)}>
{index + 1}
</button>)
)
}
This component above is a simple button-click grid with 10,000 buttons displayed. On button click we find that button and check if it's active. if yes, we switch the color to gray, otherwise, if it's not active we set the color to green.
This line is very interesting:
const [blocks, setBlocks] = useState(new Array(10000).fill({active: false}))
Here we create a new array of 10k objects.
So imagine, React would perform state changes by checking the value. It would be very slow. Instead, React uses shallow comparison for state change detection.
So, this piece of code is very expensive:
const onClickHandler = (blockIndex) => {
const updatedBlocks = blocks.map((block, index) => {
return index === blockIndex ? {active: !block.active} : block
})
setBlocks(updatedBlocks);
}
Here, we create a new array every time a user clicks on the button. We know that inside that array, 9999 objects will remain the same, while only one will be changed. With that, we set the changed array in setBlocks
, and the state is changed.
Creating a new variable of state objects to keep the state immutable can be expensive sometimes. There are techniques like memoization to fix this slow behavior.
Conclusion
Immutability is a vital concept in programming. Due to its capacity to ensure predictability, and enhance reliability, it's very important.
By prohibiting the alteration of data after creation, immutability guarantees consistent behavior. It also reduces the likelihood of unexpected bugs, fostering more dependable code.
Top comments (0)