Introduction
React is in the process of deprecating a lifecycle method we used quite a bit at Okera, namely componentWillReceiveProps
. The React team’s rationale is that componentWillReceiveProps
allows developers to write code with too much opportunity to improperly leverage the functionality that this lifecycle method provides, making the upcoming time-sliced rendering features difficult to implement.
Use Cases for ‘componentWillReceiveProps'
The React team does mention that the cases for
componentWillReceiveProps
are few and overused. I agree with this assessment, and every frontend development team should take care to vet every case in which componentWillReceiveProps
is used, determining if the change management initiated in that method is truly necessary or could be computed on every render
(in which case it should be, and memoized if performance is a concern) or if other logic should instead be moved to componentDidUpdate
, especially for side effects.Even though there are not many ‘componentWillReceiveProps’ use cases, there are still some to consider. Here are a few cases where Okera has needed to leverage the ability to determine if
props
have changed:- Delayed rendering. In some cases, we want to delay the rendering that results from a change in
props
, e.g. in cases where we do not want to flash a loading spinner immediately after theloading
state istrue
: we only want to show the loading spinner ifn
milliseconds have passed, to avoid spinners flashing constantly in the app. - Reconciling user input local state with external state. When using local state to manage a user’s input to form of data initially populated from an external data source, we want to additionally be able to detect when that external data source changes in order to take any necessary actions (alerting the user, merging the states, or even reinitializing entirely).
Since we do have valid use cases for detecting changes in
props
, we had to give thought to how to manage these changes in our codebase.When to use getDerivedStateFromProps
There are many cases when changes in
props
effect the code in your component. As a general rule, getDerivedStateFromProps
should be used as a very last resort, since it is the most difficult and most error-prone approach. At Okera, we determine when we need to use this lifecycle method in the following way:- Can your component simply use the
prop
directly?- If yes, favor this over all other approaches. If it is not in the correct format, can you make it the correct format before it is passed to the component?
- Can your component simply compute the data it needs from existing
props
?- If yes, recompute the data from the
props
on every render. If performance becomes a concern, use memoization or other optimizations to make this faster. Avoid using Reactstate
as a cache.
- If yes, recompute the data from the
- Assuming your component needs to take action when a
prop
changes, is this action asynchronous? In other words, does the component need to initiate a side effect that will take some time to complete?- If yes, use
componentDidUpdate
. Your component will render without the side effect having been applied, but if your component relies on asynchronous behavior, it also needs to handle the cases where the asynchronous behavior is in-flight.
- If yes, use
- Is your component uncontrolled? If so, can you refactor it into a controlled component?
- Many cases of derived state arise when a child component is trying to manage pieces of its own state while also relying on the parent for some state. Instead, have the parent manage all the state of the child.
- Assuming your component needs to take action when a
prop
changes, is this action synchronous? In other words, can the outcome be computed immediately?- If yes, use
getDerivedStateFromProps
.
- If yes, use
Only use getDerivedStateFromProps
if you answered “no” to every question except the last one.
How to use getDerivedStateFromProps
: a proposal
At Okera, we’ve codified a convention for times when it is necessary to use
getDerivedStateFromProps
. The primary difficulty when using getDerivedStateFromProps
is that the method is static
and does not provide developers the context necessary to truly determine if a change has occurred. To alleviate this, we’ve come up with the following convention describing both:- How to detect a change happened
- How to take action when a change is detected
Detecting a change happened
In order to make this detection, we leverage React’s component
state
. We use it to store the previous value of the prop
in order to compare it to the new one. If we detect a change, the prop
cache is updated. As an example:class MyComponent extends Component {
state = {
'props.myProp': this.props.myProp,
}
static getDerivedStateFromProps(props, state) {
if (props.myProp !== state['props.myProp']) {
// `myProp` changed. Take action here.
// Then, update the state cache, so new changes are detected:
return {
'props.myProp': props.myProp,
};
}
return null;
}
}
Note: the code above assumes
myProp
is a primitive, i.e. a change happens exactly when ===
fails. Extending this to other cases is possible, however.Taking action when a change is detected
The process of taking action is simply plugged into key points of the change detection lifecycle code. It is typical that the initialization of derived state is equivalent to the computation that needs to happen when a change is detected, but this is not always the case. Extending from the change detection code above:
// Note: often, initializeMyDerivedState = computeMyDerivedState.
const initializeMyDerivedState = (myProp) => { ... }
const computeMyDerivedState = (myProp) => { ... }
class MyComponent extends Component {
state = {
myDerivedState: initializeMyDerivedState(this.props.myProp),
'props.myProp': this.props.myProp,
}
static getDerivedStateFromProps(props, state) {
if (props.myProp !== this.state['props.myProp']) {
return {
myDerivedState: computeMyDerivedState(props.myProp),
// Then, update the state cache, so new changes are detected:
'props.myProp': props.myProp,
};
}
return null;
}
}
Reminder: the code that recomputes the derived state must be synchronous. If it is asynchronous, use
componentDidUpdate
.Conclusion
Using
getDerivedStateFromProps
should be avoided as much as possible, but I hope this post gives good guidance on how and when to use it. It is my earnest hope that React provides easier, framework-supported ways to do this in the future, leading to fewer bugs in application code.Tread carefully with the implementation above: deviating at all from this approach, or forgetting a critical piece, will likely lead to subtle bugs. I’d love feedback and any suggestions on how to improve it.
Next steps include formalizing this convention into a library of some kind, that could perhaps enhance React’s
state
to allow for this kind of change detection, either via a high-order component or React hooks. Writing a linter plugin may also be helpful here, to ensure props
are cached correctly in state
and no asynchronous behavior makes its way into getDerivedStateFromProps
.Best of luck detecting changes in React!