Now what do we mean by pure? Before we get into that, let me first describe what we mean by application state.
Application State
This is essentially React's version of what most other people call the model in MVC parlance. It is the data that you are rendering.
In React, there are two common patterns for representing your application state:
- The application state is stored inside of the various view components that comprise your UI. This is done using this.state from within a your view components.
- The application state is stored completely separate from the view components. That is, the entire object graph representing your application's data model is in it's own object, separate from your view components. That state data is then hierarchically passed down to your view components as props (i.e. html attributes).
Model Mutations
A conventional MVC app might work like this:
- You update (or mutate) the model
- You re-render all or a portion of your UI to reflect those model changes
For example, with some frameworks, model updates are detected (and UI refreshes triggered) by subscribing to change events on the model. Thus, you must make sure that all of your model objects are capable of broadcasting change events (this is how Backbone and Flex work).
Other frameworks use dirty checking, comparing the old model to the new model, to figure out when the model has changed and when a UI refresh is necessary. This is how Angular works. The nice thing about this is that your model objects can be dumb. That is they do not need special "change broadcasting" plumbing.
React
React is somewhat different. It's technique is sort of but not exactly like the dirty checking scenario. But if React's dirty checking is to work correctly, you must ensure that all of your state mutations are pure.
Pure
Now, back to the whole pure thing. Whether you put your application state inside your view components or you keep your state separate from your view, React wants your state changes (i.e. model updates) to be based on pure functions.
A pure function is a function that doesn't change any state. It simply takes some arguments and returns a value. The return value is computed solely based on the arguments. It is a pure function. It has no side effects and mutates no state. Args in. Return value out. Simple.
So how can you update a model using a function that isn't allowed to update anything?
The answer, we don't technically update the model. Instead we replace the model with an updated version of itself.
As an analogy lets compare two standard JavaScript methods of the Array object: array.push() versus array.concat().
Take a look at this example using array.push():
const a1 = [1,2,3];
a1.push(4);
console.log(a1); //[1,2,3,4]
Using push, a1 is mutated in-place.
Now look at this example, using array.concat():
const a1 = [1,2,3];
const a2 = a1.concat(4)l
console.log(a1); //[1,2,3]
console.log(a2); //[1,2,3,4]
Using concat, a1 is left unchanged. But an entirely new array is returned.
So push()is a mutating method. concat() is more of a pure method.
React wants your model updates to be more like concat() and less like push().
Let's compare a conventional model update, with a React style model update. Suppose we were rendering a simple array:
updateModelConventionalWay(){
this.state.list.push("New row");
refreshUI(); //or UI is automatically refreshed on model change
}
updateModelReactWay(){
const newList = this.state.list.concat("New row");
this.setState({list:newList});
//change detected because you set the state.
}
So to summarize, React doesn't actually want you to update your model. React wants you to completely replace your model with an updated version of itself.
The reason for this is two fold.
- Updating state is what triggers React to refresh the UI.
- Dirty checks is how React is prevented from refreshing every single component on the page.
The most efficient way to do the per-component dirty checks is to use the === operator. i.e. if oldState === newState then don't re-render the component. For example, you can define, in your view component:
shouldComponentUpdate: function(nextProps, nextState) {
return nextProps.list !== this.props.list;
}
This would not work if you used push() but it would work if you used concat().
The Problem
As your application state gets more and more complex, doing these kind of updates can get cumbersome. Imagine you are developing an outlining tool. Your data model is a complex tree of nodes. You need to modify one tiny node deeply nested somewhere in the tree-structure. Making this update is super easy using good old fashioned mutations. It's not so simple using the recommended pure function techniques.
Also, you could naively make a deep copy of your entire object graph and then make the change on that. But then React would incorrectly conclude that every node in the entire tree is dirty and re-render everything. Also, you would be consuming extra memory and CPU cycles if you did it this way.
What you really need is a smart copy, that copies the part of the tree that needs to be copied but reuses any nodes that are unchanged.
But this can be non-trivial for someone unschooled in the art of functional programming.
Functional Programming
Pure functions fall under the category of functional programming. As your data structures become more complex, you will need to become a better JavaScript functional programmer. For example, we already learned that you can use concat() instead of push() for arrays.
For objects there is a similar trick. Here is the mutating way to update an object:
const o1 = {x:10,y:10};
o1.x = 5; //o1 is mutated
And here is the non-mutating way using Object.assign():
const o1 = {x:10,y:10};
const o2 = Object.assign({},o1,{x:5});
For more complex object graphs, this can get ugly fast. So there is a tool provided by React called Immutability Helpers to make it easier to update complex object graphs. You will want to check that out.
Performance
As you might guess, updating large object graphs this way can start to get expensive. This is true. But the creators of React would argue that it is even more expensive to unnecessarily re-render parts of your UI that don't need to re-render.
Also, there are libraries, like immutable.js, that provide immutable data structures (lists, maps, etc.) and functions for efficiently returning updated copies.
Work Around
With all that said, you can still use React in the old mutate-your-model kind of way if you really want to. You can do something like this:
this.state.list.push("new row");
this.forceUpdate();
The Functional Way
Proponents of React and functional programming would argue that designing your apps based on this functional style has benefits beyond efficient dirty checking. Functional programming can lead to more predictable, testable programs that are easier to understand and easier to maintain. That is unless you are totally new to functional programming, in which case the opposite may be true.
A few other parting thoughts
- In the React world, they don't use the word model. You will likely here the terms state, application state or store used instead.
- If you decide to keep your application state completely separate from your view components, and want to pursue the recommended functional approach, there is a helper library called Redux that can be used coordinate the interaction between model and view.