By Cory House
Perhaps the most common point of confusion in React today: state.
Imagine you have a form for editing a user. It’s common to create a single change handler to handle changes to all form fields. It may look something like this:
updateState(event) {
const {name, value} = event.target;
let user = this.state.user; // this is a reference, not a copy...
user[name] = value; // so this mutates state ?
return this.setState({user});
}
The concern is on line 4. Line 4 actually mutates state because the user variable is a reference to state. React state should be treated as immutable.
From the React docs:
Never mutate
this.state
directly, as callingsetState()
afterwards may replace the mutation you made. Treatthis.state
as if it were immutable.
Why?
- setState batches work behind the scenes. This means a manual state mutation may be overridden when setState is processed.
- If you declare a shouldComponentUpdate method, you can’t use a === equality check inside because the object reference will not change. So the approach above has a potential performance impact as well.
Bottom line: The example above often works okay, but to avoid edge cases, treat state as immutable.
Here are four ways to treat state as immutable:
Approach #1: Object.assign
Object.assign creates a copy of an object. The first parameter is the target, then you specify one or more parameters for properties you’d like to tack on. So fixing the example above involves a simple change to line 3:
updateState(event) {
const {name, value} = event.target;
let user = Object.assign({}, this.state.user);
user[name] = value;
return this.setState({user});
}
On line 3, I’m saying “Create a new empty object and add all the properties on this.state.user to it.” This creates a separate copy of the user object that’s stored in state. Now I’m safe to mutate the user object on line 4 — it’s a completely separate object from the object in state.
Be sure to polyfill Object.assign since it’s unsupported in IE and not transpiled by Babel. Four options to consider:
Approach #2: Object Spread
Object spread is currently a stage 3 feature, and can be transpiled by Babel. This approach is more concise:
updateState(event) {
const {name, value} = event.target;
let user = {...this.state.user, [name]: value};
this.setState({user});
}
On line 3 I’m saying “Use all the properties on this.state.user to create a new object, then set the property represented by [name] to a new value passed on event.target.value”. So this approach works similarly to the Object.assign approach, but has two benefits:
- No polyfill required, since Babel can transpile
- More concise
You can even use destructuring and inlining to make this a one-liner:
updateState({target}) {
this.setState({user: {...this.state.user, [target.name]: target.value}});
}
I’m destructuring event in the method signature to get a reference to event.target. Then I’m declaring that state should be set to a copy of this.state.user with the relevant property set to a new value. I like how terse this is. This is currently my favorite approach to writing change handlers. ?
These two approaches above are the most common and straightforward ways to handle immutable state. Want more power? Check out the other two options below.
Approach #3: Immutability Helper
Immutability-helper is a handy library for mutating a copy of data without changing the source. This library is suggested in React’s docs.
// Import at the top:
import update from 'immutability-helper';
updateState({target}) {
let user = update(this.state.user, {$merge: {[target.name]: target.value}});
this.setState({user});
}
On line 5, I’m calling merge, which is one of many commands provided by immutability-helper. Much like Object.assign, I pass it the target object as the first parameter, then specify the property I’d like to merge in.
There’s much more to immutability helper than this. It uses a syntax inspired from MongoDB’s query language and offers a variety of powerful ways to work with immutable data.
Approach #4: Immutable.js
Want to programatically enforce immutability? Consider immutable.js. This library provides immutable data structures.
Here’s an example, using an immutable map:
// At top, import immutable
import { Map } from 'immutable';
// Later, in constructor...
this.state = {
// Create an immutable map in state using immutable.js
user: Map({ firstName: 'Cory', lastName: 'House'})
};
updateState({target}) {
// this line returns a new user object assuming an immutable map is stored in state.
let user = this.state.user.set(target.name, target.value);
this.setState({user});
}
There are three basic steps above:
- Import immutable.
- Set state to an immutable map in the constructor
- Use the set method in the change handler to create a new copy of user.
The beauty of immutable.js: If you try to mutate state directly, it will fail. With the other approaches above, it’s easy to forget, and React won’t warn you when you mutate state directly.
The downsides of immutable?
- Bloat. Immutable.js adds 57K minified to your bundle. Considering libraries like Preact can replace React in only 3K, that’s hard to accept.
- Syntax. You have to reference object properties via strings and method calls instead of directly. I prefer user.name over user.get(‘name’).
- YATTL (Yet another thing to learn) — Anyone joining your team needs to learn yet another API for getting and setting data, as well as a new set of datatypes.
A couple other interesting alternatives to consider:
Warning: Watch Out For Nested Objects!
Option #1 and #2 above (Object.assign and Object spread) only do a shallow clone. So if your object contains nested objects, those nested objects will be copied by reference instead of by value. So if you change the nested object, you’ll mutate the original object. ?
Be surgical about what you’re cloning. Don’t clone all the things. Clone the objects that have changed. Immutability-helper (mentioned above) makes that easy. As do alternatives like immer, updeep, or a long list of alternatives.
You might be tempted to reach for deep merging tools like clone-deep, or lodash.merge, but avoid blindly deep cloning.
Here’s why:
- Deep cloning is expensive.
- Deep cloning is typically wasteful (instead, only clone what has actually changed)
- Deep cloning causes unnecessary renders since React thinks everything has changed when in fact perhaps only a specific child object has changed.
Thanks to Dan Abramov for the suggestions I’ve mentioned above:
Final Tip: Consider Using Functional setState
One other wrinkle can bite you:
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.
Since setState calls are batched, code like this leads to a bug:
updateState({target}) {
this.setState({user: {...this.state.user, [target.name]: target.value}});
doSomething(this.state.user) // Uh oh, setState merely schedules a state change, so this.state.user may still have old value
}
If you want to run code after a setState call has completed, use the callback form of setState:
updateState({target}) {
this.setState((prevState) => {
const updatedUser = {...prevState.user, [target.name]: target.value}; // use previous value in state to build new state...
return { user: updatedUser }; // And what I return here will be set as the new state
}, () => this.doSomething(this.state.user); // Now I can safely utilize the new state I've created to call other funcs...
);
}
My Take
I admire the simplicity and light weight of option #2 above: Object spread. It doesn’t require a polyfill or separate library, I can declare a change handler on a single line, and I can be surgical about what has changed. ? Working with nested object structures? I currently prefer Immer.
Have other ways you like to handle state in React? Please chime in via the comments!
Looking for More on React? ⚛
I’ve authored multiple React and JavaScript courses on Pluralsight (free trial). My latest, “Creating Reusable React Components” just published! ?
Cory House is the author of multiple courses on JavaScript, React, clean code, .NET, and more on Pluralsight. He is principal consultant at reactjsconsulting.com, a Software Architect at VinSolutions, a Microsoft MVP, and trains software developers internationally on software practices like front-end development and clean coding. Cory tweets about JavaScript and front-end development on Twitter as @housecor.