An increasingly popular way to use React is in conjunction with immutable data structures—that is, data structures that never change after they are instantiated.
Whether immutability is right for your application, and if so, which immutability library to adopt, depends on the specifics of your use case. In this chapter we will cover the benefits and costs of immutability, and look at three different libraries you can use to incorporate it into your React application.
The most clear-cut benefit of immutability in a React application is what it does for shouldComponentUpdate
. Although shouldComponentUpdate
can provide great performance improvements, those improvements are often eroded or even erased as the shouldComponentUpdate
handler itself increases in complexity.
Suppose you have a shouldComponentUpdate
, which only needs to check one field in props
to determine whether it will return true
or false
. That’s going to run quickly! Now suppose instead it needs to check a dozen fields instead, and some of those fields contain object which each require multiple comparisons, since all that checking can add up fast.
The root of the problem here is that it is time-consuming to determine whether two mutable objects represent equivalent values. But what if instead that were a quick check?
With immutable props
and state
, there is indeed a quick check you can use. When your props
are represented as an immutable object, then they can no longer be updated in place; to get new props, you must instantiate a new immutable object and replace the old object with the new. As such, using oldProps !== newProps
as your shouldComponentUpdate
check will very quickly tell you if you definitely need to update.
It’s worth noting that although this check is very fast, it can generate false positives. For example, if your old state were { username: "Don Jacko", active: true }
and you invoked replaceState
passing a freshly-instantiated object { username: "Don Jacko", active: true }
, the old state
would clearly be identical to the new state,
, but a shouldComponentUpdate
function using oldState !== newState
as its test would still consider them different, and would re-render unnecessarily.
In practice, this rarely seems to come up, and if it does, the only cost would be an unnecessary re-render. False negatives (in which a component actually needed to re-render but determined that it did not) would be a much greater concern, but fortunately that problem does not manifest with this approach.
Although immutable data structures can get you dead-simple and lightning-fast shouldComponentUpdate
implementations, which save considerable rendering performance, they are not without their performance costs. Whereas, mutable objects are quick to update but slower to compare, immutable objects are quick to compare but slower to update.
Instead of making the change in place, the immutability library you’re using must instantiate at least one new object, and modify it as necessary until it matches the old object in every way except for the one changed field. This costs time both during the update process and during garbage collection, when the additional objects are inevitably cleaned up.
For the vast majority of React applications, this is an excellent tradeoff to make. Remember that a single update typically results in a call to many render functions, and that render functions instantiate quite a few objects themselves in the course of building up their return values. Sacrificing a bit of update speed to save numerous render function calls is almost always going to be a performance win.
Separate from performance, another cost to consider is that you lose the convenience of setState
and setProp
. Since both of these rely on being able to mutate your state
and props
objects, respectively, when you switch those to immutable objects, your only options become replaceState
and replaceProps
.
Besides performance benefits, there are additional architectural benefits that come from using immutable data more. These benefits extend beyond props
and state
, and apply more broadly to your application as a whole.
Immutable data is generally less error-prone to work with than mutable data. When passing mutable data from one function to another, and expecting that it will come back unchanged, you have two options: one is to clone the object ahead of time and pass the clone, and the other is to cross your fingers and hope the other function doesn’t modify it. (Because if it does, you’re in for some bug hunting!)
Defensive cloning is effective in situations like these, but generally less efficient than what an immutable library would do in the same spot. Knowing that your initial data is already assumed to be immutable can allow for faster cloning, than when it’s mutable the whole way through.
Embracing immutability means you do not have to remember to defensively clone on a case-by-case basis; instead, you will get the same safe-to-pass characteristics by virtue of using immutable data in the usual way. This makes immutable data less error-prone to work with.
The easiest way to introduce immutability to your React application is to use the Immutability Helpers Addon. It lets you continue using your existing mutable data structures as though they were immutable, by making it easy to create new (also mutable) objects instead of performing in-place updates.
While this does not actually give you any new guarantees, it does allow you to write a quick shouldComponentUpdate
function as described above.
Let’s update our example to use the Immutability Helpers Addon:
var
update
=
React
.
addons
.
update
;
var
SurveyEditor
=
React
.
createClass
(
{
// ...
handleDrop
:
function
(
ev
)
{
var
questionType
=
ev
.
dataTransfer
.
getData
(
'questionType'
)
;
var
questions
=
update
(
this
.
state
.
questions
,
{
$push
:
[
{
type
:
questionType
}
]
}
)
;
this
.
setState
(
{
questions
:
questions
,
dropZoneEntered
:
false
}
)
;
}
,
handleQuestionChange
:
function
(
key
,
newQuestion
)
{
var
questions
=
update
(
this
.
state
.
questions
,
{
$splice
:
[
[
key
,
1
,
newQuestion
]
]
}
)
;
this
.
setState
(
{
questions
:
questions
}
)
;
}
,
handleQuestionRemove
:
function
(
key
)
{
var
questions
=
update
(
this
.
state
.
questions
,
{
$splice
:
[
[
key
,
1
]
]
}
)
;
this
.
setState
(
{
questions
:
questions
}
)
;
}
// ...
}
)
;
Next we’ll try a different library, one which actually does make guarantees about immutability.
The seamless-immutable library is not part of the official React family of libraries, but was designed to be used with React. It creates immutable versions of regular Objects and Arrays, which can be passed around, accessed, and iterated over just like their mutable counterparts, but which block any operations that would mutate them.
Like the Immutability Helpers Addon, seamless-immutable provides convenience functions for working with immutable data. In the case of seamless-immutable, however, these are implemented as methods on the objects themselves. For example, seamless-immutable objects gain a .merge()
method which works like the $merge
option in the Immutable Helpers Addon.
var
update
=
React
.
addons
.
update
;
var
SurveyEditor
=
React
.
createClass
(
{
// ...
handleDrop
:
function
(
ev
)
{
var
questionType
=
ev
.
dataTransfer
.
getData
(
'questionType'
)
;
var
questions
=
this
.
state
.
questions
.
concat
(
[
{
type
:
questionType
}
]
)
;
this
.
replaceState
(
this
.
state
.
merge
(
{
questions
:
questions
,
dropZoneEntered
:
false
}
)
)
;
}
,
handleQuestionChange
:
function
(
key
,
newQuestion
)
{
var
questions
=
this
.
state
.
questions
.
map
(
function
(
question
,
index
)
{
return
index
===
key
?
newQuestion
:
question
;
}
)
;
this
.
setState
(
{
questions
:
questions
}
)
;
}
,
handleQuestionRemove
:
function
(
key
)
{
var
questions
=
this
.
state
.
questions
.
filter
(
function
(
question
,
index
)
{
return
index
!==
key
;
}
)
;
this
.
setState
(
{
questions
:
questions
}
)
;
}
// ...
}
)
;
Whereas the previous two libraries provided tools around normal JavaScript objects and arrays, Immutable.js takes a different approach: it provides alternative data structures to objects and arrays.
The most commonly-used of these data structures are Immutable.Map
(analogous to Object) and Immutable.Vector
(analogous to Array). Neither of these can be freely substituted for JavaScript objects or arrays in the general case, although you can easily convert to and from the Immutable.js data structures and their mutable JavaScript counterparts.
Immutable.Map
Immutable.Map
can be used as a substitute for regular JavaScript objects:
var
question
=
Immutable
.
Map
({
description
:
'who is your favorite superhero?'
});
// get values from the Map with .get
question
.
get
(
'description'
);
// updating values with .set returns a new object.
// The original object remains intact.
question2
=
question
.
set
(
'description'
,
'Who is your favorite comicbook hero?'
);
// merge 2 objects with .merge to get a third object.
// Once again none of the original objects are mutated.
var
title
=
{
title
:
'Question #1'
};
var
question3
=
question
.
merge
(
question2
,
title
);
question3
.
toObject
();
// { title: 'Question #1', description: 'who is your favorite comicbook hero' }
Immutable.Vector
Use Immutable.Vector
for arrays:
var
options
=
Immutable
.
Vector
(
'Superman'
,
'Batman'
);
var
options2
=
options
.
push
(
'Spiderman'
);
options2
.
toArray
();
// ['Superman', 'Batman', 'Spiderman']
You can also nest the data structures:
var
options
=
Immutable
.
Vector
(
'Superman'
,
'Batman'
);
var
question
=
Immutable
.
Map
({
description
:
'who is your favorite superhero?'
,
options
:
options
});
Immutable.js has many more facets. For more information on Immutable.js go to https://github.com/facebook/immutable-js.
In this chapter we learned about the benefits and costs of using immutability in a React application. We covered its impact on shouldComponentUpdate
, on what it means for update times, and three different ways to introduce immutability into a React code base.
Next we’ll look into other uses of React beyond traditional web applications.
13.58.148.186