2018.08.05
Dual-mode React components with getDerivedStateFromProps
Over the past year React received a few new features that make developing apps easier. One of them is the getDerivedStateFromProps
method, a replacement componentWillReceiveProps
. I don't need it 95% of the time, but when I do, I follow this pattern:
props.value
state.value
props.defaultValue
- A fallback value
I presented it in a tweet some time ago, but I didn't provide too much context back then:
When using getDerivedStateFromProps I found this order to be the best:
— FatFisz (@FatFisz) 7 czerwca 2018
1. props.value - it's highest priority
2. state.value
3. props.defaultValue - feeds state.value only initially
4. fallback default - not required, but sometimes helps (e.g. an empty string or false)
How did I get there? Let me go a few months back...
Dual mode components with componentWillReceiveProps
One of the first things you learn in React is that components can receive data through props, and that they can also manage an internal state. After that you learn about <input>
and then things become interesting: it turns out that <input>
can work both with props (in the controlled mode) or without them, only by keeping a state (uncontrolled mode).
One day I needed to build a component with the same capability. In the basic version it had to take care of itself by using state, and optionally it could be controlled through props. To simplify things, the component would always use state, and any changes in props would be reflected in the state. It sounds nice, but how can we do that with React?
Before getDerivedStateFromProps
the only sensible method was componentWillReceiveProps
, for two reasons. First, it was the method of setting the state in reaction to prop changes. Second, the other method called on updates before render, componentWillUpdate
, couldn't contain calls to setState
. At the time when this was written, those things were still described in the React docs.
Taking it all into consideration, I wrote something like this:
class Foo extends Component { state = { value: this.props.value, }; componentWillReceiveProps({ value }) { if (value != null) { this.setState({ value }); } } handleChange = value => { if (this.props.value == null) { this.setState({ value }); } if (this.props.onChange) { this.props.onChange(value); } } }
... and it worked 😉 Let's take a moment to understand why.
In the uncontrolled mode the value in props is always undefined
, so componentWillReceiveProps
will do nothing. When handleChange
is called, setState
schedules a state update - and that's it. onChange
may or may not be called, but we don't care, since value won't be passed through props anyway.
In the controlled mode, first value
is taken from props - componentWillReceiveProps
is not called at mount time, so value
needs to be set manually. Then, when props change, the value in the state is updated. When handleChange
is called, state is not changed; onChange
should be used to pass the new value through props.
This code works, but there are some obvious drawbacks:
- There are two pipelines for props feeding into state: on construction, and also when the props change (because
componentWillReceiveProps
is only called on updates) - There are two conditions which make the code more complicated (checking if the value in props is nullish)
- The conditions are similar, but have reversed signs (
==
and!=
), making it easier to make a mistake
Enter getDerivedStateFromProps
The new lifecycle method, getDerivedStateFromProps
, was introduced to avoid the pitfalls of componentWillReceiveProps
. It so happens that it can greatly simplify dual-mode components. Let me throw some code at you:
class Foo extends Component { static getDerivedStateFromProps(props, state) { // I'm using nullish-coalescing operator here for brevity: // https://github.com/tc39/proposal-nullish-coalescing return { value: props.value ?? state.value, }; } state = {}; handleChange = value => { this.setState({ value }); if (this.props.onChange) { this.props.onChange(value); } } }
This code behaves the same as the componentWillReceiveProps
version, but it looks so much cleaner!
The best change in my opinion is that now the state gets value from only one place.
getDerivedStateFromProps
is invoked right before calling the render method, both on the initial mount and on subsequent updates. (source)
Because getDerivedStateFromProps
is the only source of truth for state.value
, it's also easier to extend it - which we'll do in a minute.
The second change is that now the annoying conditions are gone; there's only one straightforward condition in the expression props.value ?? state.value
, but handleChange
doesn't have it anymore. Why? Because if the state is changed unnecessarily, getDerivedStateFromProps
will always correct it.
So that's how we get to the order of the first two elements on my list. The value in props is always the most important one because we wouldn't want props to be ignored. The value in state is used as a fallback mechanism - when the component is not controlled, state.value
is just set to itself and reacts to setState
properly.
Extending the list
The pattern I suggest has two more elements: props.defaultValue
and also "a fallback value". Here's an example code for that:
class Foo extends Component { static getDerivedStateFromProps(props, state) { return { value: ( props.value ?? state.value ?? props.defaultValue ?? '' // this can be any other value, e.g. 0, false ), }; } state = {}; }
Let's take a look at props.defaultValue
. During the mounting phase, when no value is passed through the props, state.value
is still undefined
. That's why just before the first render state.value
will be assigned the value of props.defaultValue
. On subsequent renders state.value
will be used, and props.defaultValue
will become dormant.
In a situation when neither value
nor defaultValue
are in the props, I tend to provide an appropriate value, like an empty string or a zero, as a fallback. This has two benefits:
- It silences warnings when using
<input>
, because now it is always controlled. - It ensures that the value in state will always be defined and non-null, which may simplify the logic of component's methods.
That's it!
Final thoughts
Don't use getDerivedStateFromProps
unless you really need to. Even though with this method you can make a dual-mode component easily, in most cases there's just no need to do so. I only do that for some reusable components, and not before I need them to handle both modes.
While the order of values in getDerivedStateFromProps
has significance, I don't always use all of them. Usually props.value
, state.value
, and a fallback value are sufficient. Again, add what you need when you need it, don't overcomplicate the component at the beginning.
Thanks for reading, please share your thoughts on this in the comments!