Why Should I Care About Immutable Data In ReactJS?
When I started getting into ReactJS, everyone (and every document) stressed "immutable data." You can never mutate the state! You have to overwrite the state properties with new trees of data. But, I felt like there was very little explanation as to why immutable data is helpful. What I finally realized is, from a ReactJS standpoint, immutable data isn't helpful. In fact, ReactJS doesn't care about your data at all. Immutable data is a benefit to the developer only; and, to that extent, only when he or she chooses to leverage it.
Run this demo in my JavaScript Demos project on GitHub.
At first, I thought maybe the immutable data was used to drive some "magical" process in the ReactJS framework. So, I took it at face value that immutable data was a significant value-add. But, the more that I thought about what ReactJS is doing, the more that I realized that ReactJS doesn't care about your data at all. ReactJS only cares about the "Virtual DOM". Think about it, when you go to .render() your components, you're not telling ReactJS about this or that data points - you're telling ReactJS about a single Virtual DOM fragment.
Is that virtual DOM fragment hard-coded? Is it powered by static data? Is it powered by mutable state data? Is it powered by immutable data? ReactJS doesn't care. And in fact, as a developer, most of your code probably doesn't care either. In fact, much of the code that I see - period - doesn't care about mutable vs. immutable data.
Now, I'm not trying to sell you against the idea of immutable data - it does have a value. I'm just stressing the importance of thinking about why immutable data is actually important in some cases. And, to accept the fact that immutable data isn't giving you anything in other cases.
The value of immutable data often relates to optimization. Immutable data allows you to compare direct object references instead of doing deep-tree comparisons. This is much faster. And, in ReactJS, the .shouldComponentUpdate() method allows you to compare the current props and state to the "next" props and state in order to determine if the component can skip the regeneration of the virtual DOM as a performance boost. This latter point wouldn't make sense with mutable data as the before and after references are actually the same.
ReactJS also provides a .componentWillReceiveProps() method for comparing the current props to the "next" props. This method is not concerned with optimization but rather with setting state based on the delta in props. Of course, if you are using mutable data, the delta may not make any sense as the before and after props references are the same.
So, here's my take-away: If you are using .shouldComponentUpdate() or .componentWillReceiveProps(), then immutable data is a clear value-add. And, if you are not using those methods, immutable data is an overhead (both cognitive and processing). It's up to you as the developer to determine whether or not it is worthwhile. And, of course, you can always change your mind later on.
To see how and why immutable data can make sense, I've put together a small demo that purposefully mutates state. This lets you see that mutable data doesn't "break the world;" but that it also makes certain things harder.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Why Should I Care About Immutable Data In ReactJS?
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Why Should I Care About Immutable Data In ReactJS?
</h1>
<div id="content">
<!-- This content will be replaced with the React rendering. -->
</div>
<!-- Load scripts. -->
<script src="../../vendor/reactjs/react-0.13.3.min.js"></script>
<script src="../../vendor/reactjs/JSXTransformer-0.13.3.js"></script>
<script src="../../vendor/lodash/lodash-3.9.3.min.js"></script>
<script type="text/jsx">
// I manage the Demo widget.
var Demo = React.createClass({
// I provide the initial view-model and instance properties, before the
// component is mounted.
getInitialState: function() {
this.uuid = 3;
return({
widgets: [
{
id: 1,
name: "Widget 1",
likes: []
},
{
id: 2,
name: "Widget 2",
likes: []
},
{
id: 3,
name: "Widget 3",
likes: []
}
]
});
},
// ---
// PUBLIC METHODS.
// ---
// I add a like entry for the widget with the given ID.
addLike: function( widgetID ) {
// NOTE: I am mutating the state values directly rather than treating
// them as immutable data structure. Then, I am calling .setState() to
// trigger a re-calculation of the virtual DOM.
var widget = _.find(
this.state.widgets,
{
id: widgetID
}
);
widget.likes.push( new Date() );
this.setState({
widgets: this.state.widgets
});
},
// I add a new widget to the current collection. The new widget is
// automatically given a unique ID.
addWidget: function() {
// NOTE: I am mutating the state values directly rather than treating
// them as immutable data structure. Then, I am calling .setState() to
// trigger a re-calculation of the virtual DOM.
var nextID = ++this.uuid;
this.state.widgets.push({
id: nextID,
name: ( "Widget " + nextID ),
likes: []
});
this.setState({
widgets: this.state.widgets
});
},
// I delete the widget with the given ID.
deleteWidget: function( widgetID ) {
// NOTE: I am mutating the state values directly rather than treating
// them as immutable data structure. Then, I am calling .setState() to
// trigger a re-calculation of the virtual DOM.
var index = _.findIndex(
this.state.widgets,
{
id: widgetID
}
);
this.state.widgets.splice( index, 1 );
this.setState({
widgets: this.state.widgets
});
},
// I handle the click on the add-widget link.
handleAddWidget: function( event ) {
this.addWidget();
},
// I return the virtual DOM based on the current state.
render: function() {
return(
<div>
<h2>
You Have { this.state.widgets.length } Widgets!
</h2>
<p>
<a onClick={ this.handleAddWidget }>Add Widget</a>
</p>
<Widgets
widgets={ this.state.widgets }
onLike={ this.addLike }
onDelete={ this.deleteWidget }>
</Widgets>
</div>
);
}
});
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I manage the Widgets collection.
var Widgets = React.createClass({
// I return the virtual DOM based on the current state.
render: function() {
var widgets = this.props.widgets.map(
function operator( widget ) {
return(
<Widget
key={ widget.id }
widget={ widget }
onLike={ this.props.onLike }
onDelete={ this.props.onDelete }>
</Widget>
);
},
this
);
return( <ul> { widgets } </ul> );
}
});
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I manage the Widget line item.
var Widget = React.createClass({
// I get called before the components receives new props after its initial
// rendering. At this point, we can update the state based on the change in
// props.
// --
// CAUTION: If these data references are mutated directly (ie, not treated
// as immutable) anywhere in the chain, the before and after props may be
// the same value.
componentWillReceiveProps: function( newProps ) {
console.log(
"[ %s ] Component will receive props: current( %s ) vs new( %s ).",
this.props.widget.name,
this.props.widget.likes.length,
newProps.widget.likes.length
);
},
// I handle the click on the delete link.
handleDelete: function( event ) {
this.props.onDelete( this.props.widget.id );
},
// I handle the click on the like link.
handleLike: function( event ) {
this.props.onLike( this.props.widget.id );
},
// I return the virtual DOM based on the current state.
render: function() {
var likeCount = this.props.widget.likes.length;
return(
<li>
{ this.props.widget.name }
—
<a onClick={ this.handleLike }>Like ( { likeCount } )</a>
{ " or " }
<a onClick={ this.handleDelete }>Delete</a>
</li>
);
},
// I determine if the component should update based on the delta in both
// the props and the state.
// --
// CAUTION: If these data references are mutated directly (ie, not treated
// as immutable) anywhere in the chain, the before and after props may be
// the same value.
shouldComponentUpdate: function( newProps, newState ) {
console.log(
"[ %s ] Should component update: current( %s ) vs new( %s )",
this.props.widget.name,
this.props.widget.likes.length,
newProps.widget.likes.length
);
return( true ); // Should update.
}
});
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// Render the root Demo and mount it inside the given element.
React.render( <Demo />, document.getElementById( "content" ) );
</script>
</body>
</html>
NOTE: While I am mutating state directly, I am never mutating state in an inappropriate context. Meaning, I am never directly mutating props - I am still deferring the mutation to the component that "owns" the state. This is a best practice regardless of whether or not you are using immutable data.
When I run this page, everything works as expected. I can add and remove widgets. And, I can "like" a widget and the counts all work properly. But, from the console output, you can clearly see that the .componentWillReceiveProps() and .shouldComponentUpdate() methods are compromised in their ability:
The intent of this post is not to sell you against immutable data. In fact, I think this post clearly demonstrate why it is critical in some cases. The point of this post is simply to make sure that people think about why anything is good or bad; and, not to simply accept sweeping statements without some critical thought. In ReactJS, immutable data is great, if you actually leverage it. Otherwise, it's just processing and cognitive overhead.
Want to use code from this post? Check out the license.
Reader Comments
It's interesting that you perceive immutable to be "overhead" whereas I always thought the main advantage of immutable objects is simplification. Especially in JS where objects are by default even more mutable than other languages. And especially on the client side of a client/server setup where it's extremely useful to know where your data stands compared to your last request from server. It also has some other side-effect advantages like being able to write an "undo" mechanism with almost no code.
It's also worth noting that if your data is immutable in JS, you don't need to implement shouldComponentUpdate() yourself, you can either compose objects as immutable, inherit a common immutable class, or use the legacy mixin to improve performance automatically. This is not an insignificant advantage, as avoiding the dom-diffing altogether is a huge win. React itself might not care about immutable data, but not needing to use React's VirtualDom diffing at all is even better.
@Jonathan,
I think it might make some things simple, like an Undo or easily being able to determine changes based solely on object references. But, it certainly has an overhead. From the mundane - making data changes more complex without including something like Immutable.js or React-with-addons, to the cognitive, as in requiring either a new syntax (such as the MongoDB-inspired immutability helpers) or just having to think about your data in a different way.
But, really, the main point that I'm driving at is, of things like creating an undo mechanism and overriding shouldComponentUpdate(), how much do you actually see people doing this? Granted, my ReactJS experience is *quite limited* so, I may very well be wrong. But, I've seen very little of this in posts that I've read, or even in internal code. I think that, even in the ReactJS community, these are "advanced" usages. Even the documentation doesn't really talk much about shouldComponentUpdate() until you get into the advanced performance topics.
Now, I'm not saying those things don't have a place - I'm just saying that those are the cases that make immutability helpful; and, that not all use-cases leverage immutable state in a meaningful way.
It's like using "Isolate scope" for all directives in AngularJS. It doesn't make sense in some cases.
And, I guess, more than anything, this post is a reaction to the fact that I didn't quite understand how immutability was being used by ReactJS as I never felt that it was explained well. So, I just wanted to try an explain it a bit more in-depth. If for no other reason than to augment people's mental models.
You know I love me a "mental model" :D
I guess it's like that quote from that video I sent you, "simplicity is hard work!" but I agree that it's not the best thing all the time. A wise Jedi once said "Only the Sith deal in absolutes".
@Jonathan,
It would probably be good to actually do a follow-up post on Immutable.js since that seems to be the most popular one.
@All,
I did a quick follow-up on the .shouldComponentUpdate() method:
www.bennadel.com/blog/2904-shouldcomponentupdate-will-short-circuit-an-entire-subtree-of-components-in-reactjs.htm
It was not immediately obvious to me that the .shouldComponentUpdate() method will short-circuit the update of an entire subtree of components. But, as it does, this makes it dove-tail quite nicely with the concept of immutable data.
Hi Ben,
I agree on the statement that immutable data introduces cognitive overhead (for most people at least). However, you don't need immutability necessarily to minimize your renders. You can achieve the same with a library like mobservable (which applies TFRP in the background) which provides (imho) a simpler mental model. Basically you keep your mutable data structures. Components subscribe to changes on those in the background when rendering after which they update when needed. I blogged about it a while back, you might find it interesting:
https://www.mendix.com/tech-blog/making-react-reactive-pursuit-high-performing-easily-maintainable-react-apps/
Thanks for the article Ben - I shared it in the latest issue of React Digest.
http://reactdigest.net/digests/8