Note: This article is translated from React's official blog (article address: Update on Async Rendering)
Mainly describes the update direction after React, and summarizes the problems arising from the previous life cycle. Later React will gradually abandon some life cycles and increase some more practical and more realistic life cycles. Some solutions are also proposed for migrating from the traditional life cycle to the new version of React.
For more than a year, the React team has been working on asynchronous rendering. Last month, in his speech in JSConf Iceland, Dan reveals some exciting new possibilities for asynchronous rendering . Now, we want to share with you some of the lessons we learned in learning these features, as well as some ways to help you prepare components for asynchronous rendering at startup.
One of the biggest problems we have learned is that some of our traditional component life cycles lead to unsafe coding practices. They are:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
These life cycle approaches are often misunderstood and abused. In addition, we anticipate that their potential abuse may have greater problems with asynchronous rendering. Therefore, we will add a "UNSAFE_" prefix to these lifecycles in the upcoming release. (Here, "insecurity" does not refer to security, but rather to code that uses these lifecycles will be more likely to have defects in future React versions, especially once asynchronous rendering is enabled.
Official website description
React follows semantic version control So this change will be gradual. Our current plan is:
- 16.3: Introduce the aliases UNSAFE_component WillMount, UNSAFE_component WillReceive Props and UNSAFE_component WillUpdate for the unsafe life cycle. (Old life cycle names and new aliases can be used in this version.)
- Future 16.x versions: Disable warnings are enabled for component WillMount, component WillReceive Props and component WillUpdate. (Old life cycle names and new aliases can be used in this version, but old names record DEV schema warnings.)
- 17.0: Delete component WillMount, component WillReceive Props and component WillUpdate. (From now on, only the new UNSAFE_ life cycle name will work.)
Note that if you're a React application developer, you don't have to do anything about legacy methods. The main purpose of the upcoming 16.3 release is to allow open source project maintainers to update their libraries before any abandonment warnings. These warnings will not be enabled until future 16.x releases are released.
We have maintained over 50,000 React components on Facebook, and we do not intend to rewrite them immediately. We know that migration takes time. We will adopt a step-by-step migration path and everyone in the React community.
Migration from traditional life cycle
If you want to start using the new component API introduced in React 16.3 (or if you are a maintenance person updating the library in advance), here are some examples that we hope will help you start thinking about component changes. Over time, we plan to add additional "recipes" to the document to show how to perform common tasks in a way that avoids a problematic lifecycle.
Before we begin, we will briefly outline life cycle changes for the 16.3 release plan:
- 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.
The following is the translation of the above
- We are adding the following life cycle aliases:
- UNSAFE_componentWillMount
- UNSAFE_componentWillReceiveProps
- UNSAFE_component WillUpdate (old life cycle names and new aliases will be supported)
- We introduced two new life cycles, getDerived State FromProps and getSnapshot BeforeUpdate.
New life cycle: getDerived StateFromProps
class Example extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
// ...
}
}
The new static getDerived StateFromProps lifecycle is invoked after components are instantiated and new props are received. It can return an object to update the state, or null to indicate that the new props do not require any state updates.
Together with component DidUpdate, this new life cycle should cover all use cases of traditional component WillReceive Props.
New life cycle: getSnapshot BeforeUpdate
class Example extends React.Component {
getSnapshotBeforeUpdate(prevProps, prevState) {
// ...
}
}
The new getSnapshot BeforeUpdate life cycle is invoked before the update (for example, before the DOM is updated). The return value of this lifecycle is passed as the third parameter to componentDidUpdate. (This life cycle is not often required, but can be used to manually save the scroll position during recovery.)
Together with componentDidUpdate, this new lifecycle will cover all use cases of the old version of componentWillUpdate.
Here are some examples of using them
- Initializing state
- Fetching external data
- Adding event listeners (or subscriptions) (adding event listeners)
- Updating state based on props (updating state based on props)
- Invoking external callbacks
- Side effects on props change
- Fetching external data when props change (getting external data when props change)
- Reading DOM properties before an update
Note: For brevity, the following example is written using experimental class attribute transformation, but without it, the same migration strategy is applied.
Initialization state:
This example shows a component that calls component WillMount with setState:
// Before
class ExampleComponent extends React.Component {
state = {};
componentWillMount() {
this.setState({
currentColor: this.props.defaultColor,
palette: 'rgb',
});
}
}
The simplest refactoring of this type of component is to move state initialization to a constructor or property initializer, as follows:
// After
class ExampleComponent extends React.Component {
state = {
currentColor: this.props.defaultColor,
palette: 'rgb',
};
}
Getting external data
The following is an example of a component that uses componentWillMount to obtain external data:
// Before
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
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 ...
}
}
}
The above code is problematic for server rendering (where external data is not used) and the upcoming asynchronous rendering mode where requests may be started multiple times.
For most use cases, the recommended upgrade path is to move data extraction into component DidMount:
// After
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
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 ...
}
}
}
A common misconception is that extracting from component WillMount avoids the first empty rendering. In practice, this is never true, because React always performs rendering immediately after component WillMount. If the data is unavailable during the componentWillMount trigger time, the first rendering will still show the loading status wherever you extract the data. That's why in most cases moving extraction to component DidMount has no obvious effect.
Add time monitoring
Following is an example of a component that listens to external event schedulers at installation time:
// Before
class ExampleComponent extends React.Component {
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,
});
};
}
Unfortunately, this can lead to memory leaks in server rendering (component WillUnmount will never be invoked) and asynchronous rendering (rendering may be interrupted before rendering is complete, resulting in component WillUnmount not being invoked).
It is often believed that component WillMount and component WillUnmount are always paired, but this is not guaranteed. Only after calling component DidMount can React ensure that component WillUnmount is called later for cleanup.
For this reason, the recommended way to add event monitoring is to use the component DidMount life cycle:
// After
class ExampleComponent extends React.Component {
state = {
subscribedValue: this.props.dataSource.value,
};
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,
});
};
}
Sometimes it's important to update the listener to respond to changes in attributes. If you use a library like Redux or MobX, the container components of the library will be handled for you. For application authors, we created a small library called create-subscription to help solve this problem. We will release it with React 16.3.
Instead of passing the listened dataSource prop as in the previous example, we can use create-subscription to pass the listened value.
import {createSubscription} from 'create-subscription';
const Subscription = createSubscription({
getCurrentValue(sourceProp) {
// Return the current value of the subscription (sourceProp).
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.
sourceProp.subscribe(handleSubscriptionChange);
// Return an unsubscribe method.
return function unsubscribe() {
sourceProp.unsubscribe(handleSubscriptionChange);
};
},
});
// Rather than passing the subscribable source to our ExampleComponent,
// We could just pass the subscribed value directly:
`<Subscription source={dataSource}>`
{value => `<ExampleComponent subscribedValue={value} />`}
`</Subscription>`;
Note: Libraries like Relay / Apollo should manually manage subscriptions using the same technology as creating subscriptions (referenced here) and adopt the optimizations that best suit their libraries.
Updating state based on props
Following is an example of a component that uses the old version of component WillReceive Props life cycle to update its status based on new prop values:
// Before
class ExampleComponent extends React.Component {
state = {
isScrollingDown: false,
};
componentWillReceiveProps(nextProps) {
if (this.props.currentRow !== nextProps.currentRow) {
this.setState({
isScrollingDown:
nextProps.currentRow > this.props.currentRow,
});
}
}
}
Although the above code itself is not a problem, the component WillReceive Props life cycle is often misused to solve problems. Therefore, this method will be discarded.
Starting with version 16.3, the recommended way to update state in response to props changes is to use the new static getDerived State FromProps lifecycle. (The life cycle is invoked at component creation and every time a new prop is received):
// After
class ExampleComponent extends React.Component {
// Initialize state in constructor,
// Or with a property initializer.
state = {
isScrollingDown: false,
lastRow: null,
};
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.currentRow !== prevState.lastRow) {
return {
isScrollingDown:
nextProps.currentRow > prevState.lastRow,
lastRow: nextProps.currentRow,
};
}
// Return null to indicate no change to state.
return null;
}
}
You may notice in the above example that props. current Row is a mirror state (such as state. last Row). This allows getDerived StateFromProps to access previous props values as in component WillReceive Props.
You may wonder why we are not just passing the previous props as parameters to getDerivedStateFromProps. We considered this option when designing the API, but ultimately decided against it for two reasons:
1. When getDerivedStateFromProps is first called (after instantiation), the prevProps parameter will be null, and if-not-null checks need to be added when accessing prevProps.
2. Without passing previous props to this function, a step in releasing memory in future versions of React is taken. (If React does not need to pass previous props to the life cycle, it does not need to keep previous prop objects in memory.)
Note: If you're writing shared components, react-lifecycles-compat polyfill enables the new getDerived StateFromProps lifecycle to be used with older versions of React. Learn more about how to use it below.
Calling an external callback function
The following is an example of a component that calls an external function when the internal state changes:
// Before
class ExampleComponent extends React.Component {
componentWillUpdate(nextProps, nextState) {
if (
this.state.someStatefulValue !==
nextState.someStatefulValue
) {
nextProps.onChange(nextState.someStatefulValue);
}
}
}
Using component WillUpdate in asynchronous mode is unsafe because external callbacks may be called multiple times and updated only once. Instead, you should use the component DidUpdate life cycle, because it ensures that only one call is made for each update:
// After
class ExampleComponent extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (
this.state.someStatefulValue !==
prevState.someStatefulValue
) {
this.props.onChange(this.state.someStatefulValue);
}
}
}
Side effects of props change
Similar to the above examples, sometimes components have side effects when props are changed.
// Before
class ExampleComponent extends React.Component {
componentWillReceiveProps(nextProps) {
if (this.props.isVisible !== nextProps.isVisible) {
logVisibleChange(nextProps.isVisible);
}
}
}
Like component WillUpdate, component WillReceive Props may be called multiple times but updated only once. For this reason, it is important to avoid side effects in this method. Instead, componentDidUpdate should be used because it ensures that only one call is made for each update:
// After
class ExampleComponent extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (this.props.isVisible !== prevProps.isVisible) {
logVisibleChange(this.props.isVisible);
}
}
}
Getting external data when props changes
The following is an example of a component that extracts external data based on propsvalues:
// Before
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
componentDidMount() {
this._loadAsyncData(this.props.id);
}
componentWillReceiveProps(nextProps) {
if (nextProps.id !== this.props.id) {
this.setState({externalData: null});
this._loadAsyncData(nextProps.id);
}
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
render() {
if (this.state.externalData === null) {
// Render loading state ...
} else {
// Render real UI ...
}
}
_loadAsyncData(id) {
this._asyncRequest = asyncLoadData(id).then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
}
The recommended upgrade path for this component is to move data updates to component DidUpdate. Before rendering the new props, you can also use the new getDerived StateFromProps life cycle to clean up stale data:
// After
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
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(this.props.id);
}
componentDidUpdate(prevProps, prevState) {
if (this.state.externalData === null) {
this._loadAsyncData(this.props.id);
}
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
render() {
if (this.state.externalData === null) {
// Render loading state ...
} else {
// Render real UI ...
}
}
_loadAsyncData(id) {
this._asyncRequest = asyncLoadData(id).then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
}
Note: If you use HTTP libraries that support cancellation (such as axios), it's easy to cancel an ongoing request when uninstalling.
Read DOM properties before updating
Here is an example of a component that reads properties from the DOM before updating to keep the scrolling position in the list:
class ScrollingList extends React.Component {
listRef = null;
previousScrollOffset = null;
componentWillUpdate(nextProps, nextState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (this.props.list.length < nextProps.list.length) {
this.previousScrollOffset =
this.listRef.scrollHeight - this.listRef.scrollTop;
}
}
componentDidUpdate(prevProps, prevState) {
// If previousScrollOffset is set, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
if (this.previousScrollOffset !== null) {
this.listRef.scrollTop =
this.listRef.scrollHeight -
this.previousScrollOffset;
this.previousScrollOffset = null;
}
}
render() {
return (
`<div>`
{/* ...contents... */}
`</div>`
);
}
setListRef = ref => {
this.listRef = ref;
};
}
In the above example, component WillUpdate is used to read DOM attributes. However, for asynchronous rendering, there may be a delay between the "render" phase life cycle (such as component WillUpdate and render) and the "commit" phase life cycle (such as component DidUpdate). The scrollHeight value read from the component WillUpdate will fail if the user does something similar to resizing the window during this time.
The solution to this problem is to use the new "commit" phase life cycle getSnapshot BeforeUpdate. This method is called immediately before the data changes (for example, before updating the DOM). It can pass the value of React as a parameter to componentDidUpdate and call it as soon as the data changes.
These two life cycles can be used together like this:
class ScrollingList extends React.Component {
listRef = null;
getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (prevProps.list.length < this.props.list.length) {
return (
this.listRef.scrollHeight - this.listRef.scrollTop
);
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
this.listRef.scrollTop =
this.listRef.scrollHeight - snapshot;
}
}
render() {
return (
`<div>`
{/* ...contents... */}
`</div>`
);
}
setListRef = ref => {
this.listRef = ref;
};
}
Note: If you're writing shared components, react-lifecycles-compat polyfill enables the new getSnapshot BeforeUpdate life cycle to be used with the old version of React.
Other circumstances
In addition to some of the above common examples, there may be other situations not covered in this article. If you use CompoonentWillMount, CompoonentWillUpdate or CompoonentWillReceive Props in ways not covered in this blog, and are not sure how to migrate these traditional lifecycles, you can provide your code examples and our documentation, and submit a new one together. Problem. We will provide new alternative models when updating this document.
Open source project maintainer
Open source maintainers may want to know what these changes mean for shared components. What happens to components that depend on the new static getDerived StateFromProps lifecycle if the above recommendations are implemented? Do you have to release a new major version and reduce the compatibility of React 16.2 and higher?
When React 16.3 is released, we will also release a new npm package, react-lifecycles-compat. The npm package fills in components so that the new getDerived StateFromProps and getSnapshot BeforeUpdate lifecycles can also be used with older versions of React (0.14.9+).
To use this polyfill, first add it to your library as a dependency:
// Yarn
yarn add react-lifecycles-compat
// NPM
npm install react-lifecycles-compat --save
Next, update your components to use the new life cycle (as described above).
Finally, use polyfill to make components backward compatible with older versions of React:
import React from 'react';
import {polyfill} from 'react-lifecycles-compat';
class ExampleComponent extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
// Your state update logic here ...
}
}
// Polyfill your component to work with older versions of React:
polyfill(ExampleComponent);
export default ExampleComponent;