The React 16.4 update altered how the static getDerivedStateFromProps lifecycle method behaved. This method was previously only invoked if the component's props or state had changed. This can be seen in a React lifecycle diagram that Dan Abramov created. Its behaviour was altered in React 16.4. It now gets invoked unconditionally before every render of the component. The Twitter user @ManuelBieh updated Dan's diagram in this tweet to include the change. And this is a good explanation on why the change was made.

The fix caused an angry response from some in the React community—their apps worked with React 16.3 but broke when updated to React 16.4. The problems were due to a misunderstanding about getDerivedStateFromProps. The assumption was that it was an analogue for the deprecated componentWillReceiveProps method. It had the guarantee that it would only be invoked if the component's props were changing.

The following code, available here, demonstrates this misunderstanding:

class SwipeableDrawer extends React.Component {
  static getDerivedStateFromProps() {
    // Reset the maybeSwiping state every time we receive new properties.
    return {
      maybeSwiping: false,
    };
  }

  ...

The author of SwipeableDrawer wants maybeSwiping to be reset to false only when the component receives new props. They do not want to reset every time the component renders. This code breaks with React 16.4.

This episode demonstrates that it is possible in React to write code that is fragile. You can base your code on assumptions about behaviour that are not guaranteed. The components might work at the time of writing, but they could stop working later. So what guarantees do we have?

I find misunderstandings in React tend to be around the lifecycle methods and when they get invoked. The situation was complicated by the changes to the React component lifecycle that debuted in React 16.3. These were necessary for the React Fibre project, in particular to support incremental rendering.

When React decides that the subtree needs to be rendered, it performs the work in two phases: the render phase and the commit phase. The render phase is when React works out how the browser DOM needs to change to reflect the current state of the app. It does this by creating the new virtual DOM state and diffing it with the current virtual DOM state. The commit phase is when React applies those changes to the browser DOM. React Fibre required supporting incremental rendering in React and this led to the lifecycle changes in React 16.3. The invocation guarantees for certain render-phase lifecycle methods could no longer be maintained. One such guarantee was that componentWillReceiveProps only gets called when a component's props change.

I am only aware of the following guarantees around lifecycle method invocations:

  • The method componentDidMount will be invoked exactly once after the component mounts. The render and commit phases need to first complete without error.
  • The method componentDidUpdate will be invoked exactly once when the component is updated (as opposed to mounted). This is at the end of a successful commit phase.
  • The method getSnapshotBeforeUpdate will be invoked exactly once at the start of a commit phase.
  • The method componentWillUnmount will be invoked sometime after the component is removed from the virtual DOM. It is only invoked if the component successfully mounted in the first place.
  • The render phase lifecycle methods—static getDerivedStateFromProps, shouldComponentUpdate, and render—could be invoked multiple times for no clear reason at any point during rendering. (So not just because of a props or a state change.)

We can go further with the lack of guarantees for shouldComponentUpdate and the related React.memo and React.PureComponent tools. All are intended as optimizations and not as ways to prevent rendering so that your app works correctly. Your app should still behave correctly if you were to remove all their usages (although it would presumably be less responsive, since we should not prematurely optimise).

In summary, you should create boring components whenever possible. Use lifecycle methods as little as possible. Scrutinize any use of lifecycle methods or render optimizations for behaviour that is not guaranteed.


Changelog

  • 2020-08-28 Plain English and structure improvements
  • 2022-08-30 Spelling fix

# Comments

Comments on this site are implemented using GitHub Issues. To add your comment, please add it to this GitHub Issue. It will then appear below.