-
Notifications
You must be signed in to change notification settings - Fork 7.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
An update on async rendering #596
Changes from 60 commits
544c0bc
e298b47
9ff0f12
0674c34
cdb4b9e
289a2da
99fedea
49464f7
fe6b133
1314117
f005c04
06d5be4
3696388
ac23b1f
5cae7c6
f70c0dd
c1e67be
75a43aa
fb3b91f
60d65ce
f632f22
626ac42
7456327
2909738
5400338
813be17
8de7dc4
c45fb40
858c1a7
98d5a09
442591c
b1ce572
2312173
16eb646
4d16523
9905159
1ca6cfc
7408e07
3c75def
55650fc
97a109d
21fa116
92cf72d
fa34fcf
b3bf0bd
254fc8b
b0c22f7
558d576
7425aed
65b1496
6eae811
a2139de
030980e
e110ac5
ce060eb
e143823
65eca09
a3ea63a
7ced9ce
7cf5b58
9f72403
712f4de
4610392
b824bd2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
--- | ||
title: Update on Async Rendering | ||
author: [bvaughn] | ||
--- | ||
|
||
For over a year, the React team has been working to implement asynchronous rendering. Last month during his talk at JSConf Iceland, [Dan unveiled some of the exciting new possibilities async rendering unlocks](/blog/2018/03/01/sneak-peek-beyond-react-16.html). Now we'd like to share with you some of the lessons we've learned while working on these features, and some recipes to help prepare your components for async rendering when it launches. | ||
|
||
One of the biggest lessons we've learned is that some of our legacy component lifecycles tend to encourage unsafe coding practices. They are: | ||
|
||
* `componentWillMount` | ||
* `componentWillReceiveProps` | ||
* `componentWillUpdate` | ||
|
||
These lifecycle methods have often been misunderstood and subtly misused; furthermore, we anticipate that their potential misuse may be more problematic with async rendering. Because of this, we will be adding an "UNSAFE_" prefix to these lifecycles in an upcoming release. | ||
|
||
## Gradual Migration Path | ||
|
||
[React follows semantic versioning](/blog/2016/02/19/new-versioning-scheme.html), so this change will be gradual. Our current plan is: | ||
|
||
* **16.3**: Introduce aliases for the unsafe lifecycles, `UNSAFE_componentWillMount`, `UNSAFE_componentWillReceiveProps`, and `UNSAFE_componentWillUpdate`. (Both the old lifecycle names and the new aliases will work in this release.) | ||
* **16.x**: Enable deprecation warning for `componentWillMount`, `componentWillReceiveProps`, and `componentWillUpdate`. (Both the old lifecycle names and the new aliases will work in this release, but the old names will log a DEV-mode warning.) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might be clearer as "A future 16.x release after 16.3". I kinda read this as meaning that all 16.x are problematic. |
||
* **17.0**: Remove `componentWillMount`, `componentWillReceiveProps`, and `componentWillUpdate` . (Only the new "UNSAFE_" lifecycle names will work from this point forward.) | ||
|
||
**Note that if you're an application developer, you don't have to do anything about the legacy methods yet. The primary purpose of the upcoming version 16.3 release is to enable open source project maintainers to update their libraries in advance of any deprecation warnings. Those warnings will not be enabled until a future 16.x release.** | ||
|
||
We maintain over 50,000 React components at Facebook, so we understand that migrations take time. We will take the gradual migration path together with everyone in the React community. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's important to point out our migration path isn't "tell every product team to set everything aside and start fixing their components". People commonly assume that we have this kind of power at Facebook and that product teams just go ahead and spend months catching up with our recommendations (and a "small company wouldn't be able to do this"). So I would prefer if we were very clear in our wording that this is not the case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm. Gotcha. I don't think the previous wording, "we can't rewrite our apps", conveyed that. (It didn't really make sense to me.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure—you're a native speaker so I was just trying to give you the context into what I was trying to express. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I wasn't trying to criticize the wording or say it didn't sound native. I just don't think it conveyed the meaning you are wanting to convey. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, I like it |
||
|
||
--- | ||
|
||
## Migrating from Legacy Lifecycles | ||
|
||
If you'd like to start using the new component APIs introduced in React 16.3 (or if you're a maintainer looking to update your library in advance) here are a few examples that we hope will help you to start thinking about components a bit differently. Over time, we plan to add additional "recipes" to our documentation that show how to perform common tasks in a way that avoids the problematic lifecycles. | ||
|
||
Before we begin, here's a quick overview of the lifecycle changes planned for version 16.3: | ||
* We are **adding the following lifecycle aliases**: `UNSAFE_componentWillMount`, `UNSAFE_componentWillReceiveProps`, and `UNSAFE_componentWillUpdate`. (Both the old lifecycle names and the new aliases will be supported.) | ||
* We are **introducing two new lifecycles**, static `getDerivedStateFromProps` and `getSnapshotBeforeUpdate`: | ||
|
||
`embed:update-on-async-rendering/new-lifecycles-overview.js` | ||
|
||
### New lifecycle: `getDerivedStateFromProps` | ||
|
||
The new static `getDerivedStateFromProps` lifecycle is invoked after a component is instantiated as well as when it receives new props. It can return an object to update `state`, or `null` to indicate that the new `props` do not require any `state` updates. | ||
|
||
Together with `componentDidUpdate`, this new lifecycle should cover all use cases for the legacy `componentWillReceiveProps`. | ||
|
||
### New lifecycle: `getSnapshotBeforeUpdate` | ||
|
||
The new `getSnapshotBeforeUpdate` lifecycle is called right before mutations are made (e.g. before the DOM is updated). The return value for this lifecycle will be passed as the third parameter to `componentDidUpdate`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add: "This lifecycle method usually isn't necessary but is useful in cases like manually preserving scroll position during rerenders." (I'm worried that people will either overuse this or get confused because they can't imagine use cases; I think downplaying it slightly like this will help.) |
||
|
||
Together with `componentDidUpdate`, this new lifecycle should cover all use cases for the legacy `componentWillUpdate`. | ||
|
||
We'll look at examples of how both of these lifecycles can be used below. | ||
|
||
> Note | ||
> | ||
> For brevity, the examples below are written using the experimental class properties transform, but the same migration strategies apply without it. | ||
|
||
## Examples | ||
- [Initializing state](#initializing-state) | ||
- [Fetching external data](#fetching-external-data) | ||
- [Adding event listeners (or subscriptions)](#adding-event-listeners-or-subscriptions) | ||
- [Updating `state` based on props](#updating-state-based-on-props) | ||
- [Invoking external callbacks](#invoking-external-callbacks) | ||
- [Updating external data when props change](#updating-external-data-when-props-change) | ||
- [Reading DOM properties before an update](#reading-dom-properties-before-an-update) | ||
|
||
### Initializing state | ||
|
||
This example shows a component with `setState` calls inside of `componentWillMount`: | ||
`embed:update-on-async-rendering/initializing-state-before.js` | ||
|
||
The simplest refactor for this type of component is to move state initialization to the constructor or to a property initializer, like so: | ||
`embed:update-on-async-rendering/initializing-state-after.js` | ||
|
||
### Fetching 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`: | ||
`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. | ||
|
||
> Note | ||
> | ||
> Some advanced use-cases (e.g. libraries like Relay) may want to experiment with eagerly prefetching async data. An example of how this can be done is available [here](https://gist.github.com/bvaughn/89700e525ff423a75ffb63b1b1e30a8f). | ||
> | ||
> In the longer term, the canonical way to fetch data in React components will likely be based on the “suspense” API [introduced at JSConf Iceland](/blog/2018/03/01/sneak-peek-beyond-react-16.html). Both simple data fetching solutions and libraries like Apollo and Relay will be able to use it under the hood. It is significantly less verbose than either of the above solutions, but will not be finalized in time for the 16.3 release. | ||
|
||
### Adding event listeners (or subscriptions) | ||
|
||
Here is an example of a component that subscribes to an external event dispatcher when mounting: | ||
`embed:update-on-async-rendering/adding-event-listeners-before.js` | ||
|
||
Unfortunately, this can cause memory leaks for server rendering (where `componentWillUnmount` will never be called) and async rendering (where rendering might be interrupted before it completes, causing `componentWillUnmount` not to be called). | ||
|
||
People often assume that `componentWillMount` and `componentWillUnmount` are always paired, but that is not guaranteed. Only once `componentDidMount` has been called does React guarantee that `componentWillUnmount` will later be called for clean up. | ||
|
||
For this reason, the recommended way to add listeners/subscriptions is to use the `componentDidMount` lifecycle: | ||
`embed:update-on-async-rendering/adding-event-listeners-after.js` | ||
|
||
Sometimes it is important to update subscriptions in response to property changes. If you're using a library like Redux or MobX, the library's container component should handle this for you. For application authors, we've created a small library, [`create-subscription`](https://github.com/facebook/react/tree/master/packages/create-subscription), to help with this. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "For application authors, we'll created a small library [create-subscription] to help with this and will publish it with React 16.3." |
||
|
||
Rather than passing a subscribable `dataSource` prop as we did in the example above, we could use `create-subscription` to pass in the subscribed value: | ||
|
||
`embed:update-on-async-rendering/adding-event-listeners-create-subscription.js` | ||
|
||
> Note | ||
> | ||
> Libraries like Relay/Apollo should manage subscriptions manually with the same techniques as `create-subscription` uses under the hood (as referenced [here](https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3)) in a way that is most optimized for their library usage. | ||
|
||
### Updating `state` based on `props` | ||
|
||
Here is an example of a component that uses the legacy `componentWillReceiveProps` lifecycle to update `state` based on new `props` values: | ||
`embed:update-on-async-rendering/updating-state-from-props-before.js` | ||
|
||
Although the above code is not problematic in itself, the `componentWillReceiveProps` lifecycle is often mis-used in ways that _do_ present problems. Because of this, the method will be deprecated. | ||
|
||
As of version 16.3, the recommended way to update `state` in response to `props` changes is with the new `static getDerivedStateFromProps` lifecycle. (That lifecycle is called when a component is created and each time it receives new props.): | ||
`embed:update-on-async-rendering/updating-state-from-props-after.js` | ||
|
||
> Note | ||
> | ||
> If you're writing a shared component, the [`react-lifecycles-compat`](https://github.com/reactjs/react-lifecycles-compat) polyfill enables the new `getDerivedStateFromProps` lifecycle to be used with older versions of React as well. [Learn more about how to use it below.](#open-source-project-maintainers) | ||
|
||
### Invoking external callbacks | ||
|
||
Here is an example of a component that calls an external function when its internal state changes: | ||
`embed:update-on-async-rendering/invoking-external-callbacks-before.js` | ||
|
||
Sometimes people use `componentWillUpdate` out of a misplaced fear that by the time `componentDidUpdate` fires, it is "too late" to update the state of other components. This is not the case. React ensures that any `setState` calls that happen during `componentDidMount` and `componentDidUpdate` are flushed before the user sees the updated UI. In general, it is better to avoid cascading updates like this, but in some cases they are necessary (for example, if you need to position a tooltip after measuring the rendered DOM element). | ||
|
||
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). | ||
|
||
### Reading DOM properties before an update | ||
|
||
Here is an example of a component that reads a property from the DOM before an update in order to maintain scroll position within a list: | ||
`embed:update-on-async-rendering/react-dom-properties-before-update-before.js` | ||
|
||
In the above example, `componentWillUpdate` is used to read the DOM property. However with async rendering, there may be delays between "render" phase lifecycles (like `componentWillUpdate` and `render`) and "commit" phase lifecycles (like `componentDidUpdate`). If the user does something like resize the window during this time, the `scrollHeight` value read from `componentWillUpdate` will be stale. | ||
|
||
The solution to this problem is to use the new "commit" phase lifecycle, `getSnapshotBeforeUpdate`. This method gets called _immediately before_ mutations are made (e.g. before the DOM is updated). It can return a value for React to pass as a parameter to `componentDidUpdate`, which gets called _immediately after_ mutations. | ||
|
||
The two lifecycles can be used together like this: | ||
|
||
`embed:update-on-async-rendering/react-dom-properties-before-update-after.js` | ||
|
||
## 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. | ||
|
||
## Open source project maintainers | ||
|
||
Open source maintainers might be wondering what these changes mean for shared components. If you implement the above suggestions, what happens with components that depend on the new static `getDerivedStateFromProps` lifecycle? Do you also have to release a new major version and drop compatibility for React 16.2 and older? | ||
|
||
Fortunately, you do not! | ||
|
||
In support of version 16.3, we've also created a new NPM package, [`react-lifecycles-compat`](https://github.com/reactjs/react-lifecycles-compat). This package polyfills components so that the new `getDerivedStateFromProps` lifecycle will also work with older versions of React (0.14.9+). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe: "When React 16.3 is published, we'll also publish a new npm package," since people aren't meant to use it yet? (also npm should be lowercase https://css-tricks.com/start-sentence-npm/) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh. I didn't know that (NPM vs npm) |
||
|
||
To use this polyfill, first add it as a dependency to your library: | ||
|
||
```bash | ||
# Yarn | ||
yarn add react-lifecycles-compat | ||
|
||
# NPM | ||
npm install react-lifecycles-compat --save | ||
``` | ||
|
||
Next, update your components to use the new static lifecycle, `getDerivedStateFromProps`, as described above. | ||
|
||
Lastly, use the polyfill to make your component backwards compatible with older versions of React: | ||
`embed:update-on-async-rendering/using-react-lifecycles-compat.js` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// After | ||
class ExampleComponent extends React.Component { | ||
// highlight-range{1-3} | ||
state = { | ||
subscribedValue: this.props.dataSource.value, | ||
}; | ||
// highlight-line | ||
// highlight-range{1-18} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. combine this with above highlight block too? |
||
componentDidMount() { | ||
// Event listeners are only safe to add after mount, | ||
// So they won't leak if mount is interrupted or errors. | ||
this.props.dataSource.subscribe( | ||
this.handleSubscriptionChange | ||
); | ||
|
||
// External values could change between render and mount, | ||
// In some cases it may be important to handle this case. | ||
if ( | ||
this.state.subscribedValue !== | ||
this.props.dataSource.value | ||
) { | ||
this.setState({ | ||
subscribedValue: this.props.dataSource.value, | ||
}); | ||
} | ||
} | ||
|
||
componentWillUnmount() { | ||
this.props.dataSource.unsubscribe( | ||
this.handleSubscriptionChange | ||
); | ||
} | ||
|
||
handleSubscriptionChange = dataSource => { | ||
this.setState({ | ||
subscribedValue: dataSource.value, | ||
}); | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// Before | ||
class ExampleComponent extends React.Component { | ||
// highlight-range{1-10} | ||
componentWillMount() { | ||
this.setState({ | ||
subscribedValue: this.props.dataSource.value, | ||
}); | ||
|
||
// This is not safe; it can leak! | ||
this.props.dataSource.subscribe( | ||
this.handleSubscriptionChange | ||
); | ||
} | ||
|
||
componentWillUnmount() { | ||
this.props.dataSource.unsubscribe( | ||
this.handleSubscriptionChange | ||
); | ||
} | ||
|
||
handleSubscriptionChange = dataSource => { | ||
this.setState({ | ||
subscribedValue: dataSource.value, | ||
}); | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import {createSubscription} from 'create-subscription'; | ||
|
||
const Subscription = createSubscription({ | ||
getCurrentValue(sourceProp) { | ||
// Return the current value of the subscription (sourceProp). | ||
// highlight-next-line | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO the highlighting in this code sample is distracting and I can't tell why some lines are highlighted and not others. I'd just drop all the highlights in this file especially since it isn't really a before/after and ~all the lines are significant. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool. I was on the fence about this to begin with. |
||
return sourceProp.value; | ||
}, | ||
|
||
subscribe(sourceProp, callback) { | ||
function handleSubscriptionChange() { | ||
callback(sourceProp.value); | ||
} | ||
|
||
// Subscribe (e.g. add an event listener) to the subscription (sourceProp). | ||
// Call callback(newValue) whenever a subscription changes. | ||
// highlight-next-line | ||
sourceProp.subscribe(handleSubscriptionChange); | ||
|
||
// Return an unsubscribe method. | ||
// highlight-range{1-3} | ||
return function unsubscribe() { | ||
sourceProp.unsubscribe(handleSubscriptionChange); | ||
}; | ||
}, | ||
}); | ||
|
||
// Rather than passing the subscribable source to our ExampleComponent, | ||
// We could just pass the subscribed value directly: | ||
// highlight-range{1-3} | ||
<Subscription source={dataSource}> | ||
{value => <ExampleComponent subscribedValue={value} />} | ||
</Subscription>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// After | ||
class ExampleComponent extends React.Component { | ||
state = { | ||
externalData: null, | ||
}; | ||
|
||
// highlight-range{1-8} | ||
componentDidMount() { | ||
this._asyncRequest = asyncLoadData().then( | ||
externalData => { | ||
this._asyncRequest = null; | ||
this.setState({externalData}); | ||
} | ||
); | ||
} | ||
|
||
componentWillUnmount() { | ||
if (this._asyncRequest) { | ||
this._asyncRequest.cancel(); | ||
} | ||
} | ||
|
||
render() { | ||
if (this.state.externalData === null) { | ||
// Render loading state ... | ||
} else { | ||
// Render real UI ... | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// Before | ||
class ExampleComponent extends React.Component { | ||
state = { | ||
externalData: null, | ||
}; | ||
|
||
// highlight-range{1-8} | ||
componentWillMount() { | ||
this._asyncRequest = asyncLoadData().then( | ||
externalData => { | ||
this._asyncRequest = null; | ||
this.setState({externalData}); | ||
} | ||
); | ||
} | ||
|
||
componentWillUnmount() { | ||
if (this._asyncRequest) { | ||
this._asyncRequest.cancel(); | ||
} | ||
} | ||
|
||
render() { | ||
if (this.state.externalData === null) { | ||
// Render loading state ... | ||
} else { | ||
// Render real UI ... | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// After | ||
class ExampleComponent extends React.Component { | ||
// highlight-range{1-4} | ||
state = { | ||
currentColor: this.props.defaultColor, | ||
palette: 'rgb', | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Before | ||
class ExampleComponent extends React.Component { | ||
state = {}; | ||
|
||
// highlight-range{1-6} | ||
componentWillMount() { | ||
this.setState({ | ||
currentColor: this.props.defaultColor, | ||
palette: 'rgb', | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// After | ||
class ExampleComponent extends React.Component { | ||
// highlight-range{1-8} | ||
componentDidUpdate(prevProps, prevState) { | ||
if ( | ||
this.state.someStatefulValue !== | ||
prevState.someStatefulValue | ||
) { | ||
this.props.onChange(this.state.someStatefulValue); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add: