Skip to content
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

async-safe, static lifecycle hooks #6

Merged
merged 22 commits into from
Jan 19, 2018
Merged
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9818f57
Initial proposal for async-friendly, static lifecycle hooks
bvaughn Dec 8, 2017
839547f
Added a note about the prefix
bvaughn Dec 8, 2017
b7189cc
Made deprecation/rename clearer
bvaughn Dec 8, 2017
aad6845
Hopefully clarified the computeMemoizeData() placeholder example
bvaughn Dec 8, 2017
99988da
Renamed example method for improved clarity
bvaughn Dec 9, 2017
4a34d25
Added note about aEL being unsafe in cWM
bvaughn Dec 9, 2017
902d4b3
Fixed typo
bvaughn Dec 9, 2017
3df7ce6
Fixing typo
bvaughn Dec 9, 2017
1864c93
Removed Facebook-specific terminology
bvaughn Dec 9, 2017
428758b
Tweaked wording for clarity
bvaughn Dec 9, 2017
cfcb7e8
Reorganization (part 1, more to come)
bvaughn Dec 9, 2017
536084d
Added some more focused examples
bvaughn Dec 9, 2017
a1431a4
Added a comment about calling setState on a possibly unmounted component
bvaughn Dec 9, 2017
3c6132e
Cancel async request on unmount in example
bvaughn Dec 9, 2017
4425dbe
Tweaking examples based on Dan's feedback
bvaughn Dec 9, 2017
67272ce
Renamed deriveStateFromProps to getDerivedStateFromNextProps
bvaughn Dec 13, 2017
c7f6728
Renamed prefetch to optimisticallyPrepareToRender
bvaughn Dec 13, 2017
bb2d246
Added `render` to a list
bvaughn Dec 14, 2017
8618f70
Removed static optimisticallyPrepareToRender() in favor of render()
bvaughn Dec 19, 2017
8f6c20e
Renamed getDerivedStateFromNextProps to getDerivedStateFromProps
bvaughn Jan 18, 2018
e05e317
Updated when getDerivedStateFromProps is called and what its argument…
bvaughn Jan 18, 2018
7042a2a
Renamed unsafe_* to UNSAFE_*
bvaughn Jan 18, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
288 changes: 288 additions & 0 deletions text/0000-static-lifecycle-methods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
- Start Date: 2017-12-08
- RFC PR: (leave this empty)
- React Issue: (leave this empty)

# Summary

Replace error-prone render phase lifecycle hooks with static methods to make it easier to write async-compatible React components.

Provide a clear migration path for legacy components to become async-ready.

# Basic example

At a high-level, I propose the following additions/changes to the component API. (The motivations for these changes are explained below.)

```js
class ExampleComponent extends React.Component {
static deriveStateFromProps(props, state, prevProps) {
// Called before a mounted component receives new props.
// Return an object to update state in response to prop changes.
}

static prefetch(props, state) {
// Initiate async request(s) as early as possible in rendering lifecycle.
// These requests do not block `render`.
// They can only pre-prime a cache that is used later to update state.
// (This is a micro-optimization and probably not a common use-case.)
}

unsafe_componentWillMount() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any other parts of the React API that have underscores in their naming? Wondering about simply camel-casing:
unsafeComponentWillMount().

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it’s designed to make you think twice before using it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, unstable_deferredUpdates, unstable_batchedUpdates, unstable_AsyncComponent, etc.

// New name for componentWillMount()
// Indicates that this method can be unsafe for async rendering.
}

unsafe_componentWillUpdate(nextProps, nextState) {
// New name for componentWillUpdate()
// Indicates that this method can be unsafe for async rendering.
}

unsafe_componentWillReceiveProps(nextProps) {
// New name for componentWillReceiveProps()
// Indicates that this method can be unsafe for async rendering.
}
}
```

# Motivation

The React team recently added a feature flag to stress-test Facebook components for potential incompatibilities with our experimental async rendering mode ([facebook/react/pull/11587](https://github.com/facebook/react/pull/11587)). We enabled this feature flag internally so that we could:
1. Identify common problematic coding patterns with the legacy component API to inform a new async component API.
2. Find and fix async bugs before they impact end-users by intentionally triggering them in a deterministic way.
3. Gain confidence that our existing products could work in async.

I believe this internal experiment confirmed what we suspected about the legacy component API: _It has too many potential pitfalls to be safely used for async rendering._

## Common problems

Some of the most common problematic patterns that were uncovered include:
* **Initializing Flux stores in `componentWillMount`**. It's often unclear whether this is an actual problem or just a potential one (eg if the store or its dependencies change in the future). Because of this uncertainty, it should be avoided.
* **Adding event listeners/subscriptions** in `componentWillMount` and removing them in `componentWillUnmount`. This causes leaks if the initial render is interrupted (or errors) before completion.
* **Non-idempotent external function calls** during `componentWillMount`, `componentWillUpdate`, or `componentWillReceiveProps` (eg registering callbacks that may be invoked multiple times, initializing or configuring shared controllers in such a way as to trigger invariants, etc.)

## Goal

The goal of this proposal is to reduce the risk of writing async-compatible React components. I believe that can be accomplished by removing many<sup>1</sup> of the potential pitfalls in the current API while retaining important functionality the API enables. This can be done through a combination of:

1. Choosing lifecycle method names that have a clearer, more limited purpose.
2. Making certain lifecycles static to prevent unsafe access of instance properties.

<sup>1</sup> It is not possible to detect or prevent all side-effects (eg mutations of global/shared objects).

## Examples

The following example combines all of the common, potentially-problematic patterns listed above. (Based on initial feedback, I will rewrite this as several smaller, more focused examples shortly.)

```js
class ExampleComponent extends React.Component {
constructor(props) {
super(props);

this.state = {
derivedData: null,
externalData: null,
someStatefulValue: null
};
}

componentWillMount() {
asyncLoadData(this.props.someId).then(externalData =>
this.setState({ externalData })
);

// Note that this is not safe; (it can leak)
// But it is a common pattern so I'm showing it here.
addExternalEventListeners();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding event listeners in componentWillMount is a bug, right? I know that it would make SSR fail (since there's no DOM), and it seems like you say it's a problem in the Common Problems section. If that's right, a comment to that effect would be super helpful!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flux stores are external event listeners that don't need a DOM, but also SSR alone is not necessarily that common. Especially for long tail.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that's helpful context, but my question here is whether or not this line is currently considered a bug. It sounds like this document is saying yes, it is, although I'm not entirely sure. If so, I think a comment would help understanding. Does that make sense?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. This is not a good pattern. (I listed it below as a common problem we see.) I show it here only to show where it should be done instead. I've added an inline comment to this part of the example though to make it clear that it's not a pattern we recommend. 😄


this._computeMemoizedInstanceData(this.props);
}

componentWillReceiveProps(nextProps) {
this.setState({
derivedData: computeDerivedState(nextProps)
});
}

componentWillUnmount() {
removeExternalEventListeners();
}

componentWillUpdate(nextProps, nextState) {
if (this.props.someId !== nextProps.someId) {
asyncLoadData(nextProps.someId).then(externalData =>
this.setState({ externalData })
);
}

if (this.state.someStatefulValue !== nextState.someStatefulValue) {
nextProps.onChange(nextState.someStatefulValue);
}

this._computeMemoizedInstanceData(nextProps);
}

render() {
if (this.state.externalData === null) {
return <div>Loading...</div>;
}

// Render real view...
}
}
```
Copy link
Member

@gaearon gaearon Dec 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative could look like this:

class ExampleComponent extends React.Component {
  static deriveStateFromProps(props, state, prevProps) {
    if (prevProps === null || props.someValue !== prevProps.someValue) {
      return {
        derivedData: computeDerivedState(props)
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

Derived state would be shallowly merged with the state specified in the constructor.

// Somewhere in React
instance.state = Object.assign({},
  instance.state,
  instance.type.deriveStateFromProps(instance.props, instance.state, null)
);

Are we sure we don't want this behavior?

Pros:

  • No longer have to think about out-of-sync derivation (e.g. doing it in props but not in state)
  • We can easily lint against setting state based on props in constructor now (since there's never a valid use case that isn't covered by deriveStateFromProps)

Cons:

  • It may not be obvious prevProps is nullable (this is not a big issue though because if you don't check for this your code would crash before mounting, so you'd know right away)
  • It is more implicit

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about passing {} for prevProps on initial render?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@j-f1 Structure still won't be full. So if you compare some deep references then need to check existing of property. It's uglier, slower and harder to type.

class ExampleComponent extends React.Component {
  static deriveStateFromProps(props, state, prevProps) {
    if (prevProps.someValue && props.someValue.nested !== prevProps.someValue.nested) {
      return {
        derivedData: computeDerivedState(props)
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

Copy link
Collaborator Author

@bvaughn bvaughn Dec 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure we don't want this behavior?

I considered this. In some ways I like it, but I was really concerned about this:

It may not be obvious prevProps is nullable (this is not a big issue though because if you don't check for this your code would crash before mounting, so you'd know right away)

And the idea of passing {} for prevProps on initial render just pushes the problem one level deeper, as Bogdan mentions. (I think that would actually be even more confusing.)


This proposal would modify the above component as follows:

```js
class ExampleComponent extends React.Component {
constructor(props) {
super(props);

this.state = {
derivedData: null,
externalData: null,
someStatefulValue: null
};
}

static deriveStateFromProps(props, state, prevProps) {
// If derived state is expensive to calculate,
// You can compare props to prevProps and conditionally update.
return {
derivedData: computeDerivedState(props)
};
}

static prefetch(props, state) {
// Prime the async cache early.
// (Async request won't complete before render anyway.)
// If you only need to pre-fetch before mount,
// You can conditionally fetch based on state.
asyncLoadData(props.someId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example for prefetch was a little confusing to me, since it calls a method that elsewhere in the example returns a Promise of data, but prefetch then ignores that Promise. It makes sense if you understand that asyncLoadData caches the response for the next call, but that's not really obvious. Perhaps rename that method asyncCacheData or precacheData?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name is a reference to link prefetch in the browser world, since the functionality is kind of analogous. It's good feedback though. We'll see if others find the name confusing.

}

componentDidMount() {
// Event listeners are only safe to add after mount,
// So they won't leak if mount is interrupted or errors.
addExternalEventListeners();

// Wait for earlier pre-fetch to complete and update state.
// (This assumes some kind of cache to avoid duplicate requests.)
asyncLoadData(props.someId).then(externalData =>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know I'll mess this up somehow by not caching :) I really like the getInitialProps hook from Next. Would it be possible to implement something similar to that by splitting the fetch and the resolve behind the scenes?

getInitialProps() {api.fetch(url).then(data =>this.setState({data))}

  • Starts fetch as early in the lifecycle as possible
  • holds the Promise until componentDidMount
  • Resolves

This might not be technically possible, but would be very clear on where to do async work on load without the user having to resolve the result in the correct lifecycle.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if you don't cache the request though, your browser will! 😄 (Unless the response is flagged to expire immediately)

We're thinking the use of prefetch is a micro-optimization though, not something that most people will do. (In the common case, it's fine to just wait and request your data from componentDidMount because that will happen so soon after render anyway.)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough!

this.setState({ externalData })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code as is will trigger “cannot update n unmounted component” warning because it doesn’t cancel the fetch (or the callback) on unmounting. In user land people typically solve it by keeping an isMounted flag which is clumsy. Our suggestion has been to use cancelable APIs.

How does this approach address this problem, if it does at all? Seems like if we’re touching fetching it’s a good chance to make the API easier to use.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, Dan.

I think this proposal is orthogonal to the "unmounted component" case. (It doesn't make it any easier or harder.)

I tried to keep the example code here as concise as possible to avoid distracting from the proposed API changes but you're right to point out that it's probably not good to show an example that could trigger a warning.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an inline comment about this for now 😄 Just to keep the before-and-after comparison as apples-to-apples as I can.

);
}

componentDidUpdate(prevProps, prevState) {
// Callbacks (side effects) are only safe after commit.
if (this.state.someStatefulValue !== prevState.someStatefulValue) {
this.props.onChange(this.state.someStatefulValue);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a common pattern? Won’t there be two copies of the state, one owned by the current component instance and the other owned by the parent component passing in the onChange callback?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a common pattern?

Yes, I think so. But you could also swap this.props.onChange for SomeExternalStore.somethingChanged.

Won’t there be two copies of the state

Not the state, just the value in question that's being observed.

}
}

componentWillUnmount() {
removeExternalEventListeners();
}

render() {
// Memoization that doesn't go in state can be done in render.
// It should be idempotent and have no external side effects or mutations.
// Examples include incrementing unique ids,
// Lazily calculating and caching values, etc.
this._computeMemoizedInstanceData();

if (this.state.externalData === null) {
return <div>Loading...</div>;
}

// Render real view...
}
}
```

# Detailed design

## New static lifecycle methods

### `static prefetch(props: Props, state: State): void`

This method is invoked before `render` for both the initial render and all subsequent updates. It is not called during server rendering.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC componentWillMount is called in SSR. Since componentWillMount is being deprecated and replaced with prefetch and prefetch is not called in server rendering, that means that there would be no lifecycle calls on the server any more, right? Have you looked into what use cases cWM is used for on the server?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what my note in the PR description is about. I'll be adding a related RFC shortly with componentDidServerRender based on the discussions here and here. 😄

Copy link

@aickin aickin Dec 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<facepalm emoji> I just totally missed that line. Sorry!


The purpose of this method is to initiate asynchronous request(s) as early as possible in a component's rendering lifecycle. Such requests will not block `render`. They can be used to pre-prime a cache that is later used in `componentDidMount`/`componentDidUpdate` to trigger a state update.
Copy link
Collaborator Author

@bvaughn bvaughn Dec 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inspiration for this method comes from facebook/react#7671 (comment):

You would rely on an external system to keep its cache around. Like Relay or some Flux store. So when you request it again, it's available. Just like when you <link rel="prefetch" /> something you don't get a direct handle on the resource. A separate request gets the data.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be just me, but I find the prefetch-componentDidMount two-step a bit unwieldy as a component developer; it requires me to implement some caching layer and ensure I don't end up calling the data load twice, which leads to very confusing control flow. It might be nicer to let prefetch return a Promise, which would be passed in to componentDidMount/Update, though I haven't thought that through very well.

Copy link
Collaborator

@sebmarkbage sebmarkbage Dec 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems reasonable but the whole prefetching thing is also really a micro-optimization. It's a power-feature. You can just do it in componentDidMount too.

I think this touches on a bigger issue that it is hard to pass values between different life-cycles which needs a bigger API refactor.

That said, I think it is better that it is awkward because in an async scenario you can be interrupted before componentDidMount and then get rerendered. In which case you would get another prefetch call. You wouldn't want such prefetches to trigger new fetches neither.

Same thing for just any random rerender for any other reason since this will also be in the update path.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could there be a method, say async load(props, state) (or fetch) that is used for both prefetching and actually updating the state like with componentDidMount() in the example? What would happen is that it returns a Promise resolving to key/value changes to be made to the state. (To make it more convenient, it could actually be an array of state changes, to make it easily used with Promise.all())

This would also solve the issue of the unmounting component attempting to setState(). In the case of an unmounted component, once the Promise from load() resolves, its result is just ignored.

Copy link
Collaborator Author

@bvaughn bvaughn Dec 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds similar to what @philraj proposed here?

I mentioned that I think it's also somewhat common for async requests to do things like dispatch Flux store actions on completion (rather than updating state). I think it's also common to make multiple async request, and for a single request to update multiple state keys. (I'm not commenting on the merits of these design patterns, just saying that I've observed them.)

I find myself hesitant about the idea of baking handling of this async promise-resolution into React core for some reason. I can't put my finger on why exactly and so I don't trust my opinion yet. (Maybe I worry about potential complexity or confusion. Maybe the idea is just too new to me.)

Edit Whoops. Looks like you already saw and responded to that thread. (I'm reading through notifications in chronological order, so I hadn't noticed.)


Avoid introducing any non-idempotent side-effects, mutations, or subscriptions in this method. For those use cases, use `componentDidMount`/`componentDidUpdate` instead.

### `static deriveStateFromProps(props: Props, state: State, prevProps: Props): PartialState | null`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for switching from passing in nextProps in componentWillReceiveProps to using prevProps in deriveStateFromProps? It may make updating code easier to keep the same semantics and names.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can name the parameter nextProps if you'd prefer 😄 It's the same thing.


This method is invoked before a mounted component receives new props. Return an object to update state in response to prop changes.

Note that React may call this method even if the props have not changed. I calculating derived data is expensive, compare new and previous prop values to conditionally handle changes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: “If”


React does not call this method before the intial render/mount and so it is not called during server rendering.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be ideal IMHO if we could somehow coalesce this use case for initial mount and updating props. It's common for me to pull out a function like this already so that I can call it both during mount and componentWillReceiveProps, because you often need to do the same derivation during mount. I'd argue that's the more common case.

I haven't read through this in its entirety though, maybe that's addressed elsewhere.

There are also some cases (much rarer) where I use an instance variable during this derivation process. So it'd be unfortunate not to have that there, but with async maybe there are pitfalls that I'm not aware of (I should read all of this through).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree I’d like to have this called on initial mount too. Curious to learn more why this was decided against here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As proposed, the name for this method isn't clear about if it will render on mount or not. The componentWillReceiveProps hook seems clear because a component will always initially render with props, so it wouldn't need a life cycle hook. I foresee some confusion with the current name.

Having it run on initial render would be a great feature add. Just today a dev on my team asked why we end up having so much similar logic in ComponentDidMount and ComponentReceiveProps.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The componentWillReceiveProps hook seems clear because a component will always initially render with props, so it wouldn't need a life cycle hook. I foresee some confusion with the current name.

It’s clear in retrospect because you already know it. In practice I see engineers expect it to fire on initial mount very often.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's something I wouldn't have guessed. Thanks for the insight

Copy link
Collaborator Author

@bvaughn bvaughn Dec 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting an instance property (state) in a constructor isn't a side effect? Isn't that the only thing a constructor is meant for? Maybe I misunderstanding what you mean?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I frequently see things in the constructor being side-effectful, like Promises, event emitters, accessing globals etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but initializing state via the constructor (or just as a class property, which is nicer still IMO) isn't a side effect. Again, maybe I'm misunderstanding what you're saying.

Are you suggesting that we shouldn't recommend using the constructor for anything (even state) because people might use it for other things? If so, I don't think I agree.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m suggesting we make the initilization of state pure by returning from a function given some props passed in as arguments. The constructor still needs to exist for side-effectual, non-pure operations such as binding methods of the class, creating class methods as instanve variables and calling super(). It would also really help the compilation effort, as we can evaluate pure functions ahead of time given it has to return the initial state.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the benefit of removing state initialization from the constructor if other initialization si done there. How does this help compilation?


## Deprecated lifecycle methods

### `componentWillMount` -> `unsafe_componentWillMount`

This method will log a deprecation warning in development mode recommending that users either rename to `unsafe_componentWillMount` or use the new static `prefetch` method instead. It will be removed entirely in version 17.

### `componentWillUpdate` -> `unsafe_componentWillUpdate`

This method will log a deprecation warning in development mode recommending that users either rename to `unsafe_componentWillUpdate` or use the new static `prefetch` method instead. It will be removed entirely in version 17.

### `componentWillReceiveProps` -> `unsafe_componentWillReceiveProps`

This method will log a deprecation warning in development mode recommending that users either rename to `unsafe_componentWillReceiveProps` or use the new static `deriveStateFromProps` method instead. It will be removed entirely in version 17.

# Drawbacks

The current component lifecycle hooks are familiar and used widely. This proposed change will introduce a lot of churn within the ecosystem. I hope that we can reduce the impact of this change through the use of codemods, but it will still require a manual review process and testing.

This change is **not fully backwards compatible**. Libraries will need to drop support for older versions of React in order to use the new, static API. Unfortunately, I believe this is unavoidable in order to safely transition to an async-compatible world.

# Alternatives

## Try to detect problems using static analysis

It is possible to create ESLint rules that attempt to detect and warn about potentially unsafe actions inside of render-phase lifecycle hooks. Such rules would need to be very strict though and would likely result in many false positives. It would also be difficult to ensure that library maintainers correctly used these lint rules, making it possible for async-unsafe components to cause problems within an async tree.

Sebastian has also discussed the idea of side effect tracking with the Flow team. Conceptually this would enable us to know, statically, whether a method is free of side effects and mutations. This functionality does not currently exist in Flow though, and if it did there will still be an adoption problem. (Not everyone uses Flow and there's no way to gaurantee the shared components you rely on are safe.)

## Don't support async with the legacy class component API

We could leave the class component API as-is and instead focus our efforts on a new stateful, functional component API. If a legacy class component is detected within an async tree, we could revert to sync rendering mode.

There are no advanced proposals for such a stateful, functional component API that I'm aware of however, and the complexity of such a migration would likely be at least as large as this proposal.

# Adoption strategy

Begin by reaching out to prominent third-party library maintainers to make sure there are no use-cases we have failed to consider.

Assuming we move forward with the proposal, release (at least one) minor 16.x update to add deprecation warnings for the legacy lifecycles and inform users to either rename with the `unsafe_` prefix or use the new static methods instead. We'll then cordinate with library authors to ensure they have enough time to migrate to the new API in advance of the major release that drops support for the legacy lifecycles.

We will provide a codemod to rename the deprecated lifecycle hooks with the new `unsafe_` prefix.

We will also provide codemods to assist with the migration to static methods, although given the nature of the change, codemods will be insufficient to handle all cases. Manual verification will be required.

# How we teach this

Write a blog post (or a series of posts) announcing the new lifecycle hooks and explaining our motivations for the change, as well as the benefits of being async-compatible. Provide examples of how to migrate the most common legacy patterns to the new API. (This can be more detailed versions of the [basic example](#basic-example) shown in the beginning of this RFC.)

# Unresolved questions

## Can `shouldComponentUpdate` remain an instance method?

Anectdotally, it seems far less common for this lifecycle hook to be used in ways that are unsafe for async. The overwhelming common usagee of it seems to be returning a boolean value based on the comparison of current to next props.

On the one hand, this means the method could be easily codemodded to a static method, but it would be equally easy to write a custom ESLint rule to warn about `this` references to anything other than `this.props` inside of `shouldComponentUpdate`.

Beyond this, there is some concern that making this method static may complicate inheritance for certain languages/compilers.

## Can `render` remain an instance method?

There primary motivation for leaving `render` as an instance method is to allow other instance methods to be used as event handlers and ref callbacks. (It is important for event handlers to be able to call `this.setState`.) We may change the event handling API in the future to be compatible with eg error boundaries, at which point it might be appropriate to revisit this decision.

Leaving `render` as an instance method also provides a mechanism (other than `state`) on which to store memoized data.

## Other

Are there important use cases that I've overlooked that the new static lifecycles would be insufficient to handle?