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;
user[name] = value;
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.
Never mutate this.state
directly, as calling setState()
afterwards may replace the mutation you made. Treat this.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.
- object-assign
- The MDN docs
- Babel Polyfill
- Polyfill.io
Approach #2: Object Spread
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
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.
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:
import { Map } from 'immutable';
this.state = {
user: Map({ firstName: 'Cory', lastName: 'House'})
};
updateState({target}) {
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)
}
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};
return { user: updatedUser };
}, () => this.doSomething(this.state.user);
);
}