diff --git a/content/blog/2018-02-07-update-on-async-rendering.md b/content/blog/2018-02-07-update-on-async-rendering.md index 96845367..2eab75ff 100644 --- a/content/blog/2018-02-07-update-on-async-rendering.md +++ b/content/blog/2018-02-07-update-on-async-rendering.md @@ -75,12 +75,12 @@ The simplest refactor for this type of component is to move state initialization ### Fetching external data -Here is an example of a component that uses `componentWillMount` and `componentWillUpdate` to fetch external data:: +Here is an example of a component that uses `componentWillMount` to fetch external data: `embed:update-on-async-rendering/fetching-external-data-before.js` The above code 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). -The recommended upgrade path for most use cases is to move data-fetching into `componentDidMount` and `componentDidUpdate`: +The recommended upgrade path for most use cases is to move data-fetching into `componentDidMount`: `embed:update-on-async-rendering/fetching-external-data-after.js` 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. @@ -129,6 +129,18 @@ Sometimes people use `componentWillUpdate` out of a misplaced fear that by the t Either way, it is unsafe to use `componentWillUpdate` for this purpose in async mode, because the external callback might get called multiple times for a single update. Instead, the `componentDidUpdate` lifecycle should be used since it is guaranteed to be invoked only once per update: `embed:update-on-async-rendering/invoking-external-callbacks-after.js` +### Updating external data when `props` change + +Here is an example of a component that fetches external data based on `props` values: +`embed:update-on-async-rendering/updating-external-data-when-props-change-before.js` + +The recommended upgrade path for this component is to move data-updates into `componentDidUpdate`. You can also use the new `getDerivedStateFromProps` lifecycle to clear stale data before rendering the new props: +`embed:update-on-async-rendering/updating-external-data-when-props-change-after.js` + +> **Note** +> +> If you're using an HTTP library that supports cancellation, like [axios](https://www.npmjs.com/package/axios), then it's simple to cancel an in-progress request when unmounting. For native Promises, you can use an approach like [the one shown here](https://gist.github.com/bvaughn/982ab689a41097237f6e9860db7ca8d6). + ## Other scenarios While we tried to cover the most common use cases in this post, we recognize that we might have missed some of them. If you are using `componentWillMount`, `componentWillUpdate`, or `componentWillReceiveProps` in ways that aren't covered by this blog post, and aren't sure how to migrate off these legacy lifecycles, please [file a new issue against our documentation](https://github.com/reactjs/reactjs.org/issues/new) with your code examples and as much background information as you can provide. We will update this document with new alternative patterns as they come up. diff --git a/content/docs/strict-mode.md b/content/docs/strict-mode.md index 88d2950c..1fc7944a 100644 --- a/content/docs/strict-mode.md +++ b/content/docs/strict-mode.md @@ -55,7 +55,7 @@ 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, the upcoming async mode (which is not enabled by default yet) breaks rendering into multiple 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). +The commit phase is usually very fast, but rendering can be slow. For this reason, the upcoming async mode (which is not enabled by default yet) breaks 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` diff --git a/examples/update-on-async-rendering/fetching-external-data-after.js b/examples/update-on-async-rendering/fetching-external-data-after.js index 4aae7722..1e8e352d 100644 --- a/examples/update-on-async-rendering/fetching-external-data-after.js +++ b/examples/update-on-async-rendering/fetching-external-data-after.js @@ -4,39 +4,24 @@ class ExampleComponent extends React.Component { externalData: null, }; - // highlight-range{1-9} + // highlight-range{1-8} componentDidMount() { - this._currentRequest = asyncLoadData( - this.props.id, + this._asyncRequest = asyncLoadData().then( externalData => { - this._currentRequest = null; + this._asyncRequest = null; this.setState({externalData}); } ); } - // highlight-line - // highlight-range{1-11} - componentDidUpdate(prevProps, prevState) { - if (prevProps.id !== this.props.id) { - this._currentRequest = asyncLoadData( - this.props.id, - externalData => { - this._currentRequest = null; - this.setState({externalData}); - } - ); - } - } - // highlight-line - // highlight-range{1-5} + componentWillUnmount() { - if (this._currentRequest) { - this._currentRequest.cancel(); + if (this._asyncRequest) { + this._asyncRequest.cancel(); } } render() { - if (this.externalData === null) { + if (this.state.externalData === null) { // Render loading state ... } else { // Render real UI ... diff --git a/examples/update-on-async-rendering/fetching-external-data-before.js b/examples/update-on-async-rendering/fetching-external-data-before.js index ebe7c4b8..899ddcb1 100644 --- a/examples/update-on-async-rendering/fetching-external-data-before.js +++ b/examples/update-on-async-rendering/fetching-external-data-before.js @@ -4,24 +4,24 @@ class ExampleComponent extends React.Component { externalData: null, }; - // highlight-range{1-5} + // highlight-range{1-8} componentWillMount() { - asyncLoadData(this.props.id).then(externalData => - this.setState({externalData}) + this._asyncRequest = asyncLoadData().then( + externalData => { + this._asyncRequest = null; + this.setState({externalData}); + } ); } - // highlight-line - // highlight-range{1-7} - componentWillReceiveProps(nextProps) { - if (nextProps.id !== this.props.id) { - asyncLoadData(this.props.id).then(externalData => - this.setState({externalData}) - ); + + componentWillUnmount() { + if (this._asyncRequest) { + this._asyncRequest.cancel(); } } render() { - if (this.externalData === null) { + if (this.state.externalData === null) { // Render loading state ... } else { // Render real UI ... diff --git a/examples/update-on-async-rendering/updating-external-data-when-props-change-after.js b/examples/update-on-async-rendering/updating-external-data-when-props-change-after.js new file mode 100644 index 00000000..014a3c61 --- /dev/null +++ b/examples/update-on-async-rendering/updating-external-data-when-props-change-after.js @@ -0,0 +1,55 @@ +// After +class ExampleComponent extends React.Component { + state = { + externalData: null, + }; + + // highlight-range{1-13} + static getDerivedStateFromProps(nextProps, prevState) { + // Store prevId in state so we can compare when props change. + // Clear out previously-loaded data (so we don't render stale stuff). + if (nextProps.id !== prevState.prevId) { + return { + externalData: null, + prevId: nextProps.id, + }; + } + + // No state update necessary + return null; + } + + componentDidMount() { + this._loadAsyncData(); + } + + // highlight-range{1-5} + componentDidUpdate(prevProps, prevState) { + if (prevState.externalData === null) { + this._loadAsyncData(); + } + } + + componentWillUnmount() { + if (this._asyncRequest) { + this._asyncRequest.cancel(); + } + } + + render() { + if (this.state.externalData === null) { + // Render loading state ... + } else { + // Render real UI ... + } + } + + _loadAsyncData() { + this._asyncRequest = asyncLoadData(this.props.id).then( + externalData => { + this._asyncRequest = null; + this.setState({externalData}); + } + ); + } +} diff --git a/examples/update-on-async-rendering/updating-external-data-when-props-change-before.js b/examples/update-on-async-rendering/updating-external-data-when-props-change-before.js new file mode 100644 index 00000000..0b0af809 --- /dev/null +++ b/examples/update-on-async-rendering/updating-external-data-when-props-change-before.js @@ -0,0 +1,41 @@ +// Before +class ExampleComponent extends React.Component { + state = { + externalData: null, + }; + + componentDidMount() { + this._loadAsyncData(); + } + + // highlight-range{1-6} + componentWillReceiveProps(nextProps) { + if (nextProps.id !== this.props.id) { + this.setState({externalData: null}); + this._loadAsyncData(); + } + } + + componentWillUnmount() { + if (this._asyncRequest) { + this._asyncRequest.cancel(); + } + } + + render() { + if (this.state.externalData === null) { + // Render loading state ... + } else { + // Render real UI ... + } + } + + _loadAsyncData() { + this._asyncRequest = asyncLoadData(this.props.id).then( + externalData => { + this._asyncRequest = null; + this.setState({externalData}); + } + ); + } +}