Skip to content

Instantly share code, notes, and snippets.

@dugagjin
Last active January 7, 2019 10:10
Show Gist options
  • Save dugagjin/5d13d9c65c1d803bd9bb0f95dc356259 to your computer and use it in GitHub Desktop.
Save dugagjin/5d13d9c65c1d803bd9bb0f95dc356259 to your computer and use it in GitHub Desktop.
react fetch example

Overview of how React works

I think it would be helpful to start with a quick overview of how React works, and how async rendering will impact class components.

Conceptually, React does work in two phases:

  • The render phase determines what changes need to be made to e.g. the DOM. During this phase, React calls render and then compares the result to the previous render.
  • The commit phase is when React applies any changes. (In the case of React DOM, this is when React inserts, updates, and removes DOM nodes.) React also calls lifecycles like componentDidMount and componentDidUpdate during this phase.

The commit phase is usually very fast, but rendering can be slow. For this reason, async mode will break the rendering work into pieces, pausing and resuming the work to avoid blocking the browser. This means that React may invoke render phase lifecycles more than once before committing, or it may invoke them without committing at all (because of an error or a higher priority interruption).

Render phase lifecycles include the following class component methods:

  • constructor
  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • setState updater functions (the first argument)

Because the above methods might be called more than once, it's important that they do not contain side-effects. Ignoring this rule can lead to a variety of problems. For example, the code snippet you showed above might fetch external data unnecessarily.

Suggested pattern

Given your first example above, I would suggest an approach like this:

class ExampleComponent extends React.Component {
  state = {};

  static getDerivedStateFromProps(nextProps, prevState) {
    // Store prevUserId in state so we can compare when props change.
    // Clear out any previously-loaded user data (so we don't render stale stuff).
    if (nextProps.userId !== prevState.prevUserId) {
      return {
        prevUserId: nextProps.userId,
        profileOrError: null,
      };
    }

    // No state update necessary
    return null;
  }

  componentDidMount() {
    // It's preferable in most cases to wait until after mounting to load data.
    // See below for a bit more context...
    this._loadUserData();
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.state.profileOrError === null) {
      // At this point, we're in the "commit" phase, so it's safe to load the new data.
      this._loadUserData();
    }
  }

  render() {
    if (this.state.profileOrError === null) {
      // Render loading UI
    } else {
      // Render user data
    }
  }

  _loadUserData() {
    // Cancel any in-progress requests
    // Load new data and update profileOrError
  }
}

With regard to the initial data fetching in componentDidMount:

Fetching data in componentWillMount (or the constructor) is problematic for both server rendering (where the external data won’t be used) and the upcoming async rendering mode (where the request might be initiated multiple times). For these reasons, we suggest moving it to componentDidMount.

There is a common misconception that fetching in componentWillMount lets you avoid the first empty rendering state. In practice this was never true because React has always executed render immediately after componentWillMount. If the data is not available by the time componentWillMount fires, the first render will still show a loading state regardless of where you initiate the fetch. This is why moving the fetch to componentDidMount has no perceptible effect in the vast majority of cases.

A couple of other thoughts

> I.e. the new derived state from the Props is not derived synchronously, but asynchronously. Given that getDerivedStateFromProps is sync, I don't see how this could work with the new API.

Just in case it's unclear, the old lifecycle, componentWillReceiveProps, was also synchronous. This means that you'd still have a render in between when the new props were set and when external data finished loading. So the suggested pattern above won't require any additional renders.

> Note that the above example is very simple, is prone to race conditions if the responses arrive out of order and does no cancellation on new values or component unmount.

Using componentWillReceiveProps exacerbates this problem, for reasons explained above. Hopefully you're using a library like Axios (or managing cancellation in some other way).

> So now that the methods we were making use of are deprecated, I wonder were we doing this wrong all the time? Is there already a better way to do this with the old API? And what is the recommended way to do this with the new API?

Legacy lifecycles (like componentWillReceiveProps) will continue to work until React version 17. Even in version 17, it will still be possible to use them, but they will be aliased with an "UNSAFE_" prefix to indicate that they might cause issues. Our preference is that people will migrate to using the new lifecycles though!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment