Pros and Cons of using immutability with React.js
React.js is not only a technology for creating dynamic user interfaces. Sure, it is unopinionated about how you structure the rest of your frontend application. But it does not mean that there are no ideas or guidelines that can help you with creating other parts of your app.
There are many ideas and technologies that align well with React.js. One of those (more general) ideas is data immutability. It comes from the functional programming world - and applied to the design of your frontend app can have many benefits.
What is it?
To understand the idea of data immutability you need to go back to the foundations of the programming. Don’t be scared, it’s really simple :).
Consider the following snippet of code:
var x = 12,
y = 12;
var object = { x: 1, y: 2 };
var object2 = { x: 1, y: 2 };
As you can see, object
and object2
are the same in terms of values. If you take a look at each key in object
and object2
and values corresponding to them, they are the same. The x
key points to 1
value both in object
and object2
objects. The y
key points to 2
values both in object
and object2
objects.
But if you’d like to check if object
and object2
are equal in your program, you’ll find that in fact, these objects are not the same!
object == object2 // false
object === object2 // false
Why is that? In fact, there are two types of equality generally worth mentioning in programming languages world:
- reference equality
- value equality
Reference equality
Objects are rather complicated data structures. They can have many keys, and those keys can point to arbitrary values. Those values can be objects too - so objects can be nested.
If you think about equality of things, you can in fact think about two things:
- does a thing mean the same as the other thing?
- is a thing exactly the same thing as the other thing?
If I would take an example from the real world, you can think about the cars. Let’s imagine you have a red sports car - and you neighbour has exactly the same car as yours. The same color, the same engine, the same brand. If a stranger would take a trip and take a walk near your home, they could say: Hey, those people have the same cars!.
Buy hey! You know that the neighbour’s car is not your car. You would not take a seat on your neighbour’s car and think it’s yours, right? Or at least you shouldn’t - if you crashed the neighbour’s car, you’d likely have an unhappy neighbour - and surely some legal problems ;).
The most visible part that distinguishes your car and the neighbour’s car are license plates. They must be different in terms of a license number, right?
In fact, JavaScript objects have those kind of license plates built-in. Such unique characteristic of each JavaScript object is called a reference.
When you compare objects in JavaScript, they are compared by a reference. That means the equality check like this:
object == object2;
object === object2;
Answers the question: Is object
is the same object as object2
? This works for assignments too. In fact, when you assign an object to variable x
, you assign a reference of an object to variable x
.
You can easily check this:
object = object2;
object == object2; // true
object === object2; // true
This have some interesting consequences. The code like this:
object = object2;
object.x = 12;
object.x; // 12
object2.x; // 12
In this case, you did a one assignment to x
key of an object
object. But since object
and object2
points to the same reference, the change is visible through both variables.
Every complex data structure in JavaScript follows the principles of reference equality. This includes lists (arrays) and objects. In fact, lists are objects too:
typeof([1,2,3]) // "object"
There are primitive values like numbers, strings, booleans or null
/ undefined
values. They follow a different kind of equality called value equality.
Value equality
As has been said before, reference equality answers the question like Is object1
the same object as object2
?.
Such checks are the simple ones. They are really efficient to make.
When it comes to JavaScript primitives, they usually can’t be nested. Such structures that cannot nest other structures are called shallow data structures. In such structures you can perform a value equality in an efficient way.
But what is a value equality?
Consider this code:
var x = 12,
y = 12;
In terms of being equal, numbers are simple beasts. You can clearly say that the value of x
variable is equal to the y
variable (12
is equal to 12
in math, right?) But if such variables followed the reference equality, they would not be the same. They are created in different places - so their references would be different. The x
‘s 12
would not be the same as the y
’s 12
. What a mess it would be!
Fortunately, numbers are primitives of the JavaScript. Primitives in JavaScript are compared using a value equality.
So it is not surprising to see that:
x == y; // true
x === y; // true
Value equality answers the question: Does a thing means the same as the other thing?
Implementing such equality would be harder with nested data structures. Objects can have arbitrary keys and values. It can contain other objects within them. To make such equality for objects, you’d need to follow the following algorithm:
// Input: an object1 and object2
// Output:
true if an object1 is equal in terms of values to object2
valueEqual(object1, object2):
object1keys = <list of keys of object1>
object2keys = <list of keys of object2>
return false if length(object1keys) != length(object2keys)
for each key in object1keys:
return false if key not in object2keys
return false if typeof(object1[key]) != typeof(object2[key])
if object1[key] is an object:
keyEqual = valueEqual(object1[key], object2[key])
return false if keyEqual != false
if object1[key] is a primitive:
return false if object1[key] != object2[key]
return true
Whew. That’s a lot of equality checks! It is a recursive algorithm - it can make thousands of equality checks to compare two objects. Such equality checks are commonly called deep equality checks.
To make things worse, this algorithm can never finish its work. This is because you can create a cyclical reference object. Constructing such object is great exercise to make sure you understand how value and reference equality - and I leave it for you.
What it has to do with React.js?
React.js components have the concept of state. This is what makes updates so easy - when the state changes, the component re-renders. React.js takes the hard work of transforming the DOM from state #1 to state #2.
React can’t assume anything about your state. You can mutate it however you want. That’s why setting state always re-renders the component - even if it’s not necessary at all.
With having your state complicated (like with nested objects) it’d be hard to check whether your state changed or not. You’d be forced to make a deep equality check, because when objects are compared using reference equaloty you can’t be sure whether the next state is changed.
So if you want to optimize React to perform updates only when necessary, you would need to make thousands of value equality checks if your state is huge. Of course, keeping your state minimal is the best React practice, but there are components which have their minimal state proportional to the number of elements they display - like lists.
The whole hassle that React.js needs to do is because in JavaScript you can mutate your objects. And mutations don’t have cool properties when it comes to checking equality.
Mutations
Data structure mutations is the natural thing in imperative programming languages like JavaScript. Most programmers thinks it’s the only way to work with your objects - object-oriented programming is all about changing state using messages, right?
When you change an object its reference stays the same. It’s quite natural - you just painted your car to blue, but license plate stayed the same, right?
var yourCar = {
color: 'red',
.. the same as neighboursCar
};
var neighboursCar = {
color: 'red',
... the same as yourCar
};
valueEqual(yourCar, neighboursCar); // true;
yourCar === neighboursCar; // false
yourCar.color = 'blue';
valueEqual(yourCar, neighboursCar); // false;
yourCar === neighboursCar; // false
As you can see, mutations do not change the results of reference equality checks. They change the results of value equality checks.
In fact, React.js does not need to have knowledge about what exactly changed. All it needs to know is whether the state changed at all or not.
While immutability does not provide easier answers to a what exactly changed problem, it provides a great answer to the is it changed at all or not question. Let’s see what an immutability actually is.
Get rid of mutations - immutability in action
Data immutability is an idea that you can apply to your code in any language. It can be shortly summarised in this way:
Whenever your object would be mutated, don’t do it. Instead, create a changed copy of it.
In fact, JavaScript has cool idioms to take a simple immutable approach in your code. Let’s see some examples without using any external libraries.
Mutating an object: Use the Object.assign({}, ...)
idiom instead
Let’s take an example of the cars that was shown before:
var yourCar = {
color: 'red',
.. the same as neighboursCar
};
var neighboursCar = {
color: 'red',
... the same as yourCar
};
If the yourCar
object would be mutated, the reference wouldn’t change.
yourCar.color = 'blue'; // reference stays the same!
You can use Object.assign
function to create a copy in a simple way:
var yourCarRepainted = Object.assign({}, yourCar, { color: 'blue' });
yourCarRepainted === yourCar; // false
In this way you can exactly tell whether your car changed or not. Every time you mutate an object, it gets a new reference this way.
The only problem is mutations that you perform but doesn’t change a value at all also creates a new object:
var yourCarRepainted = Object.assign({}, yourCar, { color: 'red' });
yourCarRepainted === yourCar; // false :(
This can have its pros in some situations - you can detect an intent of mutating an object even if with a given input nothing changes.
Mutating an array: Use [].concat
idiom instead
There is also an idiom for changing arrays in an immutable way.
Let’s take a simple example:
var list = [1, 2, 3];
Let’s imagine that you’d like to change a second value of this array:
var list = [1, 2, 3];
list[1] = 4;
This of course does not change the reference to an array. What you can do is you can copy the list using the [].concat
idiom and perform your change.
var list = [1, 2, 3];
var changedList = [].concat(list);
changedList[1] = 4;
This way a modified array has a different reference than a source list. It suffers from the same “problem” as the Object.assign
idiom - even if you doesn’t change anything in a modified array, it still gets a new reference:
var list = [1, 2, 3];
var changedList = [].concat(list);
changedList[1] = 2;
list === changedList; // false
But it depends on an use case if it’s a problem or not.
Cons of using immutable approach without an external library
While it’s a great first step to introduce immutability using this idioms in your JavaScript app, it’s better to use an optimised library which provides a set of immutable data structures for you.
While idioms presented are easy to use, they are slow. With big datasets it can be a problem - but while you do not experience performance issues with them you can transform it to something better later.
Pros and cons
Since you understand practical implications of applying immutability ideas in your code, pros and cons can be discussed.
Pro: It’s not all or nothing
This characteristic of immutability is especially important when you have a current codebase written without immutable principles.
Immutability is not a framework - it’s only an approach or an idea you can apply to pieces of your code if you want. Using those idioms you can gradually change your code to an immutable approach - starting with tiny functions, ending in whole modules.
You can also apply immutable approach if you experience performance problems with your React.js components.
Pro: You can improve performance of your components with it
React.js comes with tools to work with immutable data structures built into it. To modify your data structures in an immutable way, you can use immutability helpers shipped with React.js.
To directly improve performance of your components, there is a mixin provided with React itself called PureRenderMixin
.
It changes assumptions that React.js had before when it comes to state. If you include it in your component, React.js will not re-render with every setState
call. It is because you add another assumption about your state - everytime it changes, the reference changes. So instead it will take all keys of your state and perform a reference equality check on them and re-render only if references changed.
That can greatly improve performance in a critical parts of your app - especially in components like lists, where state can naturally grow with amount of elements displayed.
If you are not using mixins (because you use ECMAScript 2015 component class syntax for example - you can read about it here, there are plenty of options to achieve the same effect. You can use react-codemod if you are using React.createClass
syntax with PureRenderMixin
but you want to change it to ES2015 classes. You can use react-pure-render package which uses the same internals as PureRenderMixin
, but with other ways of integration (friendly with ES2015 classes) and so on.
Pro: Your data changes are more explicit
While mutability can be convenient, it can make your code more implicit. Let’s take a following example:
var object = { x: 2, y: 4 };
performSomething(object);
object.x; // ?
object.y; // ?
What values x
and y
keys have after calling performSomething
function? I don’t know. In fact, there is no way to know until you inspect the performSomething
function itself.
If you follow the immutable approach, such situations become simpler. If you mutate something in a performSomething
function, you need to return it afterwards. So the code above can’t change the object. If it’d change an object, the code snippet would look like this:
var object = { x: 2, y: 4 };
var changedObject = performSomething(object);
object.x; // 2
object.y; // 4
changedObject.x; // ?
changedObject.y; // ?
As you can see, with immutable approach you have more explicit way in code to mark that the code called changes data.
Pro: It makes certain features easier
One of the side effects you get with immutable data structures is that you always get a previous versions of your data if you want. This can be also achieved with mutable approach, but with immutable approach it’s seamless. It’s very convenient while implementing features like the history of changes or undo/redo.
It can be very helpful if you are persisting your JavaScript data structures directly - like in an isomorphic approach to web apps.
There are architectures that aligns well with immutable data approach - one of them is CQRS/Event Sourcing architecture which relies on a list of immutable facts or events to build the whole state of your app. There are also Flux libraries that leverage immutability of your data - one of the most famous nowadays is gaearon’s redux.
Con: It needs dependencies to make it right
As has been said before, as idioms described before is a great start, to make things right you need specialised libraries for immutable data structures. Things like lists, maps (objects) optimised for immutability are a great help to implement immutability ideas in your codebase.
I’d recommend the immutable-js library for it. It has nice API and it comes from Facebook itself. Another option is the baobab library - but it works better when more 'reactish’ ideas are present in your codebase, like global app state.
Con: It is less performant than mutable approach with small datasets
There is a constant performance penalty when using immutable data structures. Mutations are the most performant operations in any programming languages - your computer architecture is optimised for mutating memory in a direct way. Often this performance penalty is negligible. What’s important is the constant factor of this penalty - the bigger data you have, the more negligible the constant factor is.
Con: It needs a discipline thorough the team
Immutability is an idea. Nothing stops you from mutating objects directly. It needs programmer’s discipline to implement those ideas. It can be a problem with bigger teams. You need to have a way to propagate knowledge thorough the team and learn them how to use immutability idioms and libraries backing the immutability idea in your codebase.
It is not the problem with immutability only. There are more ideas that need some kind of human factor overhead when it comes to agreeing about standards with your team. You should always discuss such decisions with your teammates if you are not working alone.
Learn more about immutability
There is a great presentation from the ReactConf by Lee Byron which describes data immutability in detail by a great immutable-js library as an example. I recommend it - it’s a great talk! It also covers more theoretic concerns like implementation of immutable data structures.
Summary
Immutability is a very interesting idea - which is optional when you don’t want it. It has many benefits. You can decide whether you want to use it or not. It’s always hard to make a decision - I hope I made clear what implications you get when you want to use immutability ideas in your codebase.