Handling State in React: Four Immutable Approaches to Consider
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:
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:
this.statedirectly, as calling
setState()afterwards may replace the mutation you made. Treat
this.stateas if it were immutable.
- 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:
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.
Approach #2: Object Spread
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:
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
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:
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.
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:
If you want to run code after a setState call has completed, use the callback form of setState:
I admire the simplicity and light weight of option #2 above: Object spread. It doesn’t require a polyfill or separate library, and I can declare a change handler on a single line. 👍
Have other ways you like to handle state in React? Please chime in via the comments!
Looking for More on React? ⚛