3 Reasons why I stopped using React.setState
Since a few months I’ve stopped using React’s setState on all my new React components. Don’t get me wrong, I didn’t stop having local component state, I just stopped using React to manage it. And it’s been delightful!
Using setState is tricky for beginners. Even experienced React programmers easily introduce subtle bugs when using React’s own state mechanism, like this:
The excellent Reacts docs sum up everything that could go wrong when using setState:
To summarize, there are 3 issues when using setState:
1. setState is asynchronous
Many devs don’t realize this initially, but setState is asynchronous. If you set some state, than take a look at it, you will still see the old state.This is the most tricky part of setState. setState calls don’t look asynchronous, and naively calling setState introduces very subtle bugs. The next gist demonstrates that nicely:
At first glance this might look fine. Two event handlers and a utility function that fire the onSelect event if provided. However, this Select component has a bug that is nicely demonstrated in the GIF above. onSelect is always fired with the previous value of state.selection, because setState hasn’t done it’s job yet when the fireOnSelect helper is invoked. I think the least React could do here is rename the method to scheduleState or make the callback required.
This bug is easily fixed, the tricky part is realizing it’s there.
2. setState causes unnecessary renders
The second issue with setState is that it always triggers a re-render. Often those re-renders are unnecessary. You can use the printWasted method from the React performance tools to find out when this happens. But roughly speaking there are several reasons why a re-render may be unnecessary:
- The new state is actually the same as the previous one. This can often be addressed by implementing shouldComponentUpdate. You may already be using a (pure render) library to solve this for you.
- Sometimes the changed state is relevant for the rendering, but not under all circumstances. For example when some data is only conditionally visible.
- Thirdly, as pointed out in Aria Buckles’ talk at React Europe 2015, sometimes instance state is not relevant for the rendering at all! This is often householding state related to managing event listeners, timer ID’s etc.
3. setState is not sufficient to capture all component state
Following the last point above, not all component state should be stored and updated using setState. More complex components often have administration that is needed by lifecycle hooks to manage timers, network requests, events etc. Managing those with setState not only causes unnecessary renders, but also causes related lifecycle hooks to be triggered again, leading to weird situations.
Managing local component state with MobX
(Surprise, surprise) At Mendix we already rely on MobX to manage all our stores. However, we were still using React’s own state mechanism for local component state. Recently, we switched to managing local component state with MobX as well. That looks like this:
For completeness sake:
The above code snippet is not only more concise, MobX also addresses all of the setState related issues:
Changes to the state are immediately reflected in the local component state. This makes our logic simpler and code reuse easier. You don’t have to compensate for the fact that the state might not have been updated yet.
MobX determines at runtime which observables are relevant for rendering. So observables that are temporarily irrelevant for the rendering, won’t cause a re-rendering. Until they are relevant again. For this reason, there are also no rendering penalties (or lifecycle issues) when marking fields as @observable that are not relevant for rendering at all.
So renderable and non-renderable state is treated uniformly. In addition, state stored in our components now works the same as state stored in any of our stores. This makes it trivial to refactor components, and move local component state into a separate store or vice versa. Which is demonstrated in this egghead tutorial.
MobX effectively turns your components into small stores
Furthermore, rookie mistakes like assigning values directly to the state object cannot be made anymore when using observables for state. Oh, and don’t worry about implementing shouldComponentUpdate or PureRenderMixin, MobX already takes care of that as well. Finally, you might be wondering, what if I want to wait until setState has finished? Well, you can still use the compentDidUpdate lifecycle hook for that.
Sounds cool! How do I get started with MobX?
Pretty simple, follow the 10 minute interactive introduction or watch the aforementioned video. You can simply take a single component from your code base, slap @observer on it and introduce some @observable properties. You don’t even have to replace your existing setState calls, they continue to work while using MobX. Although, within a few minutes you might find them so convoluted that you will replace them anyway :). (Oh, and if you don’t like decorators, no worries, it works with good ol’ ES5 as well).
I’ve stopped using React to manage local component state. I use MobX instead. Now React is truly “just the view” :). MobX now manages both local component state and state in stores. It is concise, synchronous, efficient and uniform. From experience, I’ve learned that MobX is even easier to explain to React beginners than React’s own setState. It keeps our components clean and simple.