React Moving Away from ‘componentWillReceiveProps’

React is in the process of deprecating a lifecycle method with the emergence of React 17, namely componentWillReceiveProps, which we used quite a bit at Okera. 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. In light of this, we’ll discuss the possibility of using getDerivedStateFromProps as a componentWillReceiveProps replacement as React evolves.

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 the loading state is true: we only want to show the loading spinner if n 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 propseffect 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:

  1. 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?
  2. 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 React state as a cache.
  3. 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.
  4. 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.
  5. 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.

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 with ‘getDervicedStateFromProps’

In order to make this detection, we leverage React’s component state. We use it to store the previous value of the propin 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 after a change is detected with getDerivedStateFromProps 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!