Movatterモバイル変換


[0]ホーム

URL:


We want to hear from you!Take our 2021 Community Survey!
This site is no longer updated.Go to react.dev

Update on Async Rendering

This blog site has been archived. Go toreact.dev/blog to see the recent posts.

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. 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. (Here, “unsafe” refers not to security but instead conveys that code using these lifecycles will be more likely to have bugs in future versions of React, especially once async rendering is enabled.)

Gradual Migration Path

React follows semantic versioning, so this change will be gradual. Our current plan is:

  • 16.3: Introduce aliases for the unsafe lifecycles,UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProps, andUNSAFE_componentWillUpdate. (Both the old lifecycle names and the new aliases will work in this release.)
  • A future 16.x release: Enable deprecation warning forcomponentWillMount,componentWillReceiveProps, andcomponentWillUpdate. (Both the old lifecycle names and the new aliases will work in this release, but the old names will log a DEV-mode warning.)
  • 17.0: RemovecomponentWillMount,componentWillReceiveProps, andcomponentWillUpdate . (Only the new “UNSAFE_” lifecycle names will work from this point forward.)

Note that if you’re a React 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, and we don’t plan to rewrite them all immediately. We understand that migrations take time. We will take the gradual migration path along with everyone in the React community.

If you don’t have the time to migrate or test these components, we recommend running a“codemod” script that renames them automatically:

cd your_projectnpx react-codemod rename-unsafe-lifecycles

Learn more about this codemod on the16.9.0 release post.


This blog site has been archived. Go toreact.dev/blog to see the recent posts.

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 areadding the following lifecycle aliases:UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProps, andUNSAFE_componentWillUpdate. (Both the old lifecycle names and the new aliases will be supported.)
  • We areintroducing two new lifecycles, staticgetDerivedStateFromProps andgetSnapshotBeforeUpdate.

New lifecycle:getDerivedStateFromProps

classExampleextendsReact.Component{staticgetDerivedStateFromProps(props, state){// ...}}

The new staticgetDerivedStateFromProps lifecycle is invoked after a component is instantiated as well as before it is re-rendered. It can return an object to updatestate, ornull to indicate that the newprops do not require anystate updates.

Together withcomponentDidUpdate, this new lifecycle should cover all use cases for the legacycomponentWillReceiveProps.

Note:

Both the oldercomponentWillReceiveProps and the newgetDerivedStateFromProps methods add significant complexity to components. This often leads tobugs. Considersimpler alternatives to derived state to make components predictable and maintainable.

New lifecycle:getSnapshotBeforeUpdate

classExampleextendsReact.Component{getSnapshotBeforeUpdate(prevProps, prevState){// ...}}

The newgetSnapshotBeforeUpdate 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 tocomponentDidUpdate. (This lifecycle isn’t often needed, but can be useful in cases like manually preserving scroll position during rerenders.)

Together withcomponentDidUpdate, this new lifecycle should cover all use cases for the legacycomponentWillUpdate.

You can find their type signaturesin this gist.

We’ll look at examples of how both of these lifecycles can be used below.

Examples

Note

For brevity, the examples below are written using the experimental class properties transform, but the same migration strategies apply without it.

Initializing state

This example shows a component withsetState calls inside ofcomponentWillMount:

// BeforeclassExampleComponentextendsReact.Component{  state={};componentWillMount(){this.setState({currentColor:this.props.defaultColor,palette:'rgb',});}}

The simplest refactor for this type of component is to move state initialization to the constructor or to a property initializer, like so:

// AfterclassExampleComponentextendsReact.Component{  state={currentColor:this.props.defaultColor,palette:'rgb',};}

Fetching external data

Here is an example of a component that usescomponentWillMount to fetch external data:

// BeforeclassExampleComponentextendsReact.Component{  state={externalData:null,};componentWillMount(){this._asyncRequest=loadMyAsyncData().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 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 intocomponentDidMount:

// AfterclassExampleComponentextendsReact.Component{  state={externalData:null,};componentDidMount(){this._asyncRequest=loadMyAsyncData().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 ...}}}

There is a common misconception that fetching incomponentWillMount lets you avoid the first empty rendering state. In practice this was never true because React has always executedrender immediately aftercomponentWillMount. If the data is not available by the timecomponentWillMount fires, the firstrender will still show a loading state regardless of where you initiate the fetch. This is why moving the fetch tocomponentDidMount 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 availablehere.

In the longer term, the canonical way to fetch data in React components will likely be based on the “suspense” APIintroduced at JSConf Iceland. 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.

When supporting server rendering, it’s currently necessary to provide the data synchronously –componentWillMount was often used for this purpose but the constructor can be used as a replacement. The upcoming suspense APIs will make async data fetching cleanly possible for both client and server rendering.

Adding event listeners (or subscriptions)

Here is an example of a component that subscribes to an external event dispatcher when mounting:

// BeforeclassExampleComponentextendsReact.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 cause memory leaks for server rendering (wherecomponentWillUnmount will never be called) and async rendering (where rendering might be interrupted before it completes, causingcomponentWillUnmount not to be called).

People often assume thatcomponentWillMount andcomponentWillUnmount are always paired, but that is not guaranteed. Only oncecomponentDidMount has been called does React guarantee thatcomponentWillUnmount will later be called for clean up.

For this reason, the recommended way to add listeners/subscriptions is to use thecomponentDidMount lifecycle:

// AfterclassExampleComponentextendsReact.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 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, to help with this. We’ll publish it along with React 16.3.

Rather than passing a subscribabledataSource prop as we did in the example above, we could usecreate-subscription to pass in the subscribed 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){functionhandleSubscriptionChange(){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.returnfunctionunsubscribe(){      sourceProp.unsubscribe(handleSubscriptionChange);};},});// Rather than passing the subscribable source to our ExampleComponent,// We could just pass the subscribed value directly:<Subscriptionsource={dataSource}>{value=><ExampleComponentsubscribedValue={value}/>}</Subscription>;

Note

Libraries like Relay/Apollo should manage subscriptions manually with the same techniques ascreate-subscription uses under the hood (as referencedhere) in a way that is most optimized for their library usage.

Updatingstate based onprops

Note:

Both the oldercomponentWillReceiveProps and the newgetDerivedStateFromProps methods add significant complexity to components. This often leads tobugs. Considersimpler alternatives to derived state to make components predictable and maintainable.

Here is an example of a component that uses the legacycomponentWillReceiveProps lifecycle to updatestate based on newprops values:

// BeforeclassExampleComponentextendsReact.Component{  state={isScrollingDown:false,};componentWillReceiveProps(nextProps){if(this.props.currentRow!== nextProps.currentRow){this.setState({isScrollingDown:          nextProps.currentRow>this.props.currentRow,});}}}

Although the above code is not problematic in itself, thecomponentWillReceiveProps lifecycle is often mis-used in ways thatdo present problems. Because of this, the method will be deprecated.

As of version 16.3, the recommended way to updatestate in response toprops changes is with the newstatic getDerivedStateFromProps lifecycle. It is called when a component is created and each time it re-renders due to changes to props or state:

// AfterclassExampleComponentextendsReact.Component{// Initialize state in constructor,// Or with a property initializer.  state={isScrollingDown:false,lastRow:null,};staticgetDerivedStateFromProps(props, state){if(props.currentRow!== state.lastRow){return{isScrollingDown: props.currentRow> state.lastRow,lastRow: props.currentRow,};}// Return null to indicate no change to state.returnnull;}}

You may notice in the example above thatprops.currentRow is mirrored in state (asstate.lastRow). This enablesgetDerivedStateFromProps to access the previous props value in the same way as is done incomponentWillReceiveProps.

You may wonder why we don’t just pass previous props as a parameter togetDerivedStateFromProps. We considered this option when designing the API, but ultimately decided against it for two reasons:

  • AprevProps parameter would be null the first timegetDerivedStateFromProps was called (after instantiation), requiring an if-not-null check to be added any timeprevProps was accessed.
  • Not passing the previous props to this function is a step toward freeing up memory in future versions of React. (If React does not need to pass previous props to lifecycles, then it does not need to keep the previousprops object in memory.)

Note

If you’re writing a shared component, thereact-lifecycles-compat polyfill enables the newgetDerivedStateFromProps lifecycle to be used with older versions of React as well.Learn more about how to use it below.

Invoking external callbacks

Here is an example of a component that calls an external function when its internal state changes:

// BeforeclassExampleComponentextendsReact.Component{componentWillUpdate(nextProps, nextState){if(this.state.someStatefulValue!==      nextState.someStatefulValue){      nextProps.onChange(nextState.someStatefulValue);}}}

Sometimes people usecomponentWillUpdate out of a misplaced fear that by the timecomponentDidUpdate fires, it is “too late” to update the state of other components. This is not the case. React ensures that anysetState calls that happen duringcomponentDidMount andcomponentDidUpdate 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 usecomponentWillUpdate for this purpose in async mode, because the external callback might get called multiple times for a single update. Instead, thecomponentDidUpdate lifecycle should be used since it is guaranteed to be invoked only once per update:

// AfterclassExampleComponentextendsReact.Component{componentDidUpdate(prevProps, prevState){if(this.state.someStatefulValue!==      prevState.someStatefulValue){this.props.onChange(this.state.someStatefulValue);}}}

Side effects on props change

Similar to theexample above, sometimes components have side effects whenprops change.

// BeforeclassExampleComponentextendsReact.Component{componentWillReceiveProps(nextProps){if(this.props.isVisible!== nextProps.isVisible){logVisibleChange(nextProps.isVisible);}}}

LikecomponentWillUpdate,componentWillReceiveProps might get called multiple times for a single update. For this reason it is important to avoid putting side effects in this method. Instead,componentDidUpdate should be used since it is guaranteed to be invoked only once per update:

// AfterclassExampleComponentextendsReact.Component{componentDidUpdate(prevProps, prevState){if(this.props.isVisible!== prevProps.isVisible){logVisibleChange(this.props.isVisible);}}}

Fetching external data whenprops change

Here is an example of a component that fetches external data based onprops values:

// BeforeclassExampleComponentextendsReact.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=loadMyAsyncData(id).then(externalData=>{this._asyncRequest=null;this.setState({externalData});});}}

The recommended upgrade path for this component is to move data updates intocomponentDidUpdate. You can also use the newgetDerivedStateFromProps lifecycle to clear stale data before rendering the new props:

// AfterclassExampleComponentextendsReact.Component{  state={externalData:null,};staticgetDerivedStateFromProps(props, state){// Store prevId in state so we can compare when props change.// Clear out previously-loaded data (so we don't render stale stuff).if(props.id!== state.prevId){return{externalData:null,prevId: props.id,};}// No state update necessaryreturnnull;}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=loadMyAsyncData(id).then(externalData=>{this._asyncRequest=null;this.setState({externalData});});}}

Note

If you’re using an HTTP library that supports cancellation, likeaxios, then it’s simple to cancel an in-progress request when unmounting. For native Promises, you can use an approach likethe one shown here.

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:

classScrollingListextendsReact.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(<divref={this.setListRef}>{/* ...contents... */}</div>);}setListRef=ref=>{this.listRef= ref;};}

In the above example,componentWillUpdate is used to read the DOM property. However with async rendering, there may be delays between “render” phase lifecycles (likecomponentWillUpdate andrender) and “commit” phase lifecycles (likecomponentDidUpdate). If the user does something like resize the window during this time, thescrollHeight value read fromcomponentWillUpdate will be stale.

The solution to this problem is to use the new “commit” phase lifecycle,getSnapshotBeforeUpdate. This method gets calledimmediately before mutations are made (e.g. before the DOM is updated). It can return a value for React to pass as a parameter tocomponentDidUpdate, which gets calledimmediately after mutations.

The two lifecycles can be used together like this:

classScrollingListextendsReact.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);}returnnull;}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(<divref={this.setListRef}>{/* ...contents... */}</div>);}setListRef=ref=>{this.listRef= ref;};}

Note

If you’re writing a shared component, thereact-lifecycles-compat polyfill enables the newgetSnapshotBeforeUpdate lifecycle to be used with older versions of React as well.Learn more about how to use it below.

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 usingcomponentWillMount,componentWillUpdate, orcomponentWillReceiveProps in ways that aren’t covered by this blog post, and aren’t sure how to migrate off these legacy lifecycles, pleasefile a new issue against our documentation 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 staticgetDerivedStateFromProps lifecycle? Do you also have to release a new major version and drop compatibility for React 16.2 and older?

Fortunately, you do not!

When React 16.3 is published, we’ll also publish a new npm package,react-lifecycles-compat. This package polyfills components so that the newgetDerivedStateFromProps andgetSnapshotBeforeUpdate lifecycles will also work with older versions of React (0.14.9+).

To use this polyfill, first add it as a dependency to your library:

# Yarnyarnadd react-lifecycles-compat# NPMnpminstall react-lifecycles-compat--save

Next, update your components to use the new lifecycles (as described above).

Lastly, use the polyfill to make your component backwards compatible with older versions of React:

import Reactfrom'react';import{polyfill}from'react-lifecycles-compat';classExampleComponentextendsReact.Component{staticgetDerivedStateFromProps(props, state){// Your state update logic here ...}}// Polyfill your component to work with older versions of React:polyfill(ExampleComponent);exportdefault ExampleComponent;

Is this page useful?Edit this page

[8]ページ先頭

©2009-2025 Movatter.jp