-
Notifications
You must be signed in to change notification settings - Fork 558
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
Conversation
8c27453
to
839547f
Compare
As the RFC stands it seems written towards solving problems for one, very specific, workflow. Maybe that’s just the examples. The resulting interface though seems extremely verbose & complicated comparatively to the one it replaces. I like the goal of this though. The idea is good. |
Thanks for the input!
The goal is to solve all workflows/use-cases in a way that's safer by default (or maybe, in a way that has fewer potential pitfalls, or requires less advanced knowledge to use correctly). If you know of use-cases that it couldn't handle, this is a great time to discuss them!
Does the proposed API actually increase verbosity or complexity? I think it just moves things around, but maybe I'm stuck looking at it with a stale perspective. |
I feel this proposal has great benefits over what we have now, but ultimately should probably be merged into an entirely new component API that doesn't involve classes at all. Furthermore, to unify with our other efforts, i.e. compilation of React components, by adding more to already complicated class component will makes things even more complicated to handle. In my opinion we need a new component API that offers:
|
Thanks Dom.
I'm not sure what the right answer is, but the advantage of this proposal over what you're describing is that it provides an incremental path for existing components to take advantage of async. A completely new, non-class-based API seems both harder to design and much larger effort to adopt. That being said, I recognize that I can't appreciate the compiler difficulties presented by the class component API like you can. |
|
||
This method is invoked before `render` for both the initial render and all subsequent updates. It is not called during server rendering. | ||
|
||
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. |
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.
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.
The goal of this proposal is not to be the final "React.next" API. It's to provide a reasonable upgrade path for a large set of components e.g. in a codemoddable way. The assumption is that:
It is also much easier to adopt full async in a large codebase this way and a small ever growing portion being compiler compatible with the new API. Now whether it is worth doing both steps is arguable but that's the idea. |
Let me take a step back and state what I feel the ultimate goal would be for me: in the future, we don't have class components anymore and state/lifecycles should instead be handled by another mechanism that doesn't force us to break out a declarative render phase into an imperative OO class phase. That sounds bonkers, but will likely be a future RFC from me once I have time to properly explain it. That being said, incrementally, I feel we should start adding constraints to components:
|
@trueadm : If instance variables are no longer possible, and lifecycle methods are going away, how would React components handle non-React interop? The standard example would be: class MyPluginWrapper extends Component {
componentDidMount() {
this.$theDiv = $(this.theDiv)
this.$theDiv.somePlugin("create");
}
componentDidUpdate() {
this.$theDiv.somePlugin("update", this.props.someValue);
}
componentWillUnmount() {
this.$theDiv.somePlugin("destroy");
}
render() {
return <div ref={theDiv => this.theDiv = theDiv} />;
}
} In this use case, references to non-React values are stored as instance fields, and created/updated/destroyed in lifecycle methods. |
Thanks for your work on this RFC! I may just not have enough background in the async discussion (is there a place where those ideas are laid out?), but I have to say that I found this RFC very difficult to understand. A few suggestions that probably would have made it easier for me and maybe might make it easier for others:
Again, thanks for thinking through these issues and for all your work. Also, I apologize for the stuff I just have totally misunderstood. I'm excited to see async moving forward! |
Note: this isn't really that relevant to this PR proposal. @markerikson I'm glad you asked :) lately I've been using a lot of ReasonReact and the place for refs is most definitely state, which would fit nicely into my For more information on how ReasonReact deals with refs: https://reasonml.github.io/reason-react/docs/en/react-ref.html |
That's useful feedback for the RFC process in general. This proposal just follows the standard template.
I don't think I meant to imply any change in ordering. Could you clarify where you thought this? Actually @aickin, do you mind splitting up your comments and adding them inline? Large, multi-bullet comments are hard to respond to in a single thread like this. 😄 |
I think it was a vague impression I got from the following ideas:
That implied to me that the new static functions are called in a different way than their previous instance method analogues, which made me think that there are more changes to lifecycle order/number of calls. Honestly, though, it was a very rough impression that I may have just totally misread.
Sure, sorry! ETA: I moved my big list of nits out from this discussion into line comments in the document. I hope they are genuinely helpful, and please let me know if they are not! |
|
||
addExternalEventListeners(); | ||
|
||
this._computeMemoizeData(nextProps); |
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.
Is nextProps
defined here? I may just be missing it, but I don't see where it would be defined.
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.
Whoops. This was a typo. Will fix.
this.setState({ externalData }) | ||
); | ||
|
||
addExternalEventListeners(); |
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.
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!
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.
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.
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.
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?
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.
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. 😄
|
||
### `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. |
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.
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?
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.
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.
<facepalm emoji> I just totally missed that line. Sorry!
// (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); |
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.
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
?
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.
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.
|
||
Avoid introducing any 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` |
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.
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.
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.
You can name the parameter nextProps
if you'd prefer 😄 It's the same thing.
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 GK confirmed what we suspected about the legacy component API: _It has too many potential pitfalls to be safely used for async rendering._ |
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.
"I believe this GK confirmed ..." was a little confusing for me as a non-Facebooker. I assume it's referring to the experiment y'all did, but took me a few beats to guess that.
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.
Ah, thank you. I'll change this to use non-Facebook-specific terminology.
@trueadm : so just to clarify, the place to store anything a component needs to reference would now be in "state", even if those values aren't directly related to re-rendering? The rule of thumb so far has been "if it relates to rendering, put it in |
|
||
This method is invoked before `render` for both the initial render and all subsequent updates. It is not called during server rendering. | ||
|
||
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. |
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.
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.
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.
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.
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.
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.
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.
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.)
componentDidUpdate(prevProps, prevState) { | ||
// Callbacks (side effects) are only safe after commit. | ||
if (this.state.someStatefulValue !== prevState.someStatefulValue) { | ||
this.state.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.
this.props.onChange
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.
Whoops. Thanks.
Note: this isn't really that relevant to this PR proposal. @markerikson In my opinion, something in state doesn't have to mean it anything to do with rendering. I think we have an opportunity to change that in the future and simplify things too. I attempted to tackle these problems with Inferno and they were very received – so I feel we can do better here. |
@trueadm : I think the reason for that rule of thumb comes from this:
If you've got some ideas that work well as an alternative to that mindset, I'm certainly curious to see what you have in mind. |
Will |
It will not be called in the current implementation but it is under consideration. |
Lifecycle hooks apply to class components only. I think the existing documentation already makes this pretty clear. Once we add docs for the new (static) lifecycle methods we'll hopefully make that clear as well. The docs are a community effort too, so if you think it's not clear enough- PRs are welcome. Edit: I've added a TODO to my PR to detect and warn about this case in DEV mode. |
The RFC is slightly out of date with respect to a couple of things. (I'll probably update it once I've posted an implementation PR.) This is one of them. Our current thinking is that |
Really glad to see this being discussed because it's a point we've debated/worked on a great deal 👍 This is a fairly lengthy thread so apologies if my comments end up repeating any of the points I've missed here or in the various code reviews.
|
This method will be called at the same time and in the same cases as
As mentioned in the comment above yours, our current thinking is that
I understand why you mention it, but I don't think this (already pretty noisy) discussion is the best place to have this chat. 😄 |
Thanks for the quick response @bvaughn
Is there scope for extending this proposal to include consideration of
Sorry, I've just now seen that response. Makes total sense. Sounds like
Noted 👍 - perhaps I can encourage you / @gaearon / others to chime in on either the linked issue or the React Discuss thread? |
It seems like you're concerned about the price of computing updated I don't think this negates the value of As for the other discussion thread, I'm sure one of us will try to comment when we can. There's a lot of discussion threads and not many of us though. 😅 Thanks for bring it to attention again. Sorry! |
if (this.state.externalData === null) { | ||
// Prime an external cache as early as possible. | ||
// (Async request would not complete before render anyway.) | ||
asyncLoadData(this.props.someId); |
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.
I realize why this is being done, but should side effects in render
really be encouraged for such a common pattern?
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.
Hopefully it will be pretty rare, and componentDidMount
will work for most use cases.
But I wonder, why don't you begin loading async data in the constructor? Would that be encouraged?
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.
I was hesitant to show this pattern on the RFC, because it's only intended for rare, advanced use cases (eg a library like Relay). I'm a little bummed that it's drawn a bit of negative attention, but I understand why. I should have probably labeled it much more clearly with a disclaimer or something.
That being said, you could trigger the load in the constructor. Side effects in the constructor feel worse to me than side effects in render
, but neither is great. Timing wise though, it would be about the same. (render
will be called synchronously, shortly after the constructor.)
I really approve of this proposal! When it comes to |
You make a very fair point; we do indeed end up comparing fields between So I think my "unease" simplifies then to the incongruence of But then perhaps I'm only pushing this because I was surprised when I first stumbled across this in the current implementation!
No apology needed :) |
For what it's worth, the docs do point this out 😄
|
🤦♂️ - I think I'll go and take a break :) Thanks, I must have missed this before. |
No worries. It happens~ |
This RFC has been implemented via facebook/react/pull/12028 which was merged this morning. |
When we can expect 16.3 release? |
To be determined. |
|
||
React does not call this method before the intial render/mount and so it is not called during server rendering. | ||
Note that React may call this method even if the props have not changed. If calculating derived data is expensive, compare next and previous props to conditionally handle changes. |
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.
@bvaughn The PR for React included
a note that getDerivedStateFromProps
doesn't receive previous props. Should this sentence be updated?
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.
The wording here is a bit ambiguous but I think that's okay. Current and previous props can be compared, as shown in this example
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.
Ah, previous props in this case means props that have been synced to the local state and can be access via prevState
, right? Alright then, I thought this was left over since I think the prevProps
parameter got removed in a later iteration.
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.
So @bvaughn does this mean that, given our previous conversation, I have to "sync" all my props to state in order to compare previous props with the new props? Feels like I'm missing something here.
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.
Not all props
, only ones that are directly used to derive state
, and only if it's an expensive computation.
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.
Does storing "mirrored" props in state in addition to their derivatives not go against the React principle of state as a minimal representation?
It's a bit different though. You're not storing a "mirrored" version, you're storing the previous version. Which is a special case of an accumulation (that happens to completely discard the current value). Accumulation is exactly the purpose of getDerivedStateFromProps
and since you already added it (presumably for accumulating something), adding another field to it doesn't hurt.
I guess one could say that a "mirrored" version would also be a special case of accumulation (that discards the previous value) but that's sophism IMO :-)
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.
since you already added it (presumably for accumulating something), adding another field to it doesn't hurt
Yeah, I see what you mean. I just feel that it muddies the message of "don't put unnecessary stuff in state" where "necessary" no longer precisely means "used in render
". Personally I find this a bigger / less intuitive exception to a rule than prevProps
's nullability (the reason for which is pretty obvious -- no prevProps
on first render). I suspect newer devs would struggle more with the former.
Anyway I see the trade-off, and I appreciate your willingness to explain! 😊
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.
I just feel that it muddies the message of "don't put unnecessary stuff in state" where "necessary" no longer precisely means "used in render".
We might actually revisit this one ^^
With async React it might not be safe to put certain things on the instance fields. See facebook/react#10580.
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.
@bvaughn @gaearon I've tried to flesh out what I mean using a Typescript example - see this gist - to compare the two approaches.
I've extracted this example from a real life component that has more props, but hopefully you get the idea: any component would have to follow a similar pattern.
There are three files in the gist:
- the component written as per this proposal
- the component written assuming a signature of
static getDerivedStateFromProps(nextProps: Props, prevProps: Props, initial: boolean): State
- the diff between the two
Notice file 2 is using a slightly different signature to the proposed in my last response; I agree with @gaearon the nullability issue is best avoided, hence prevProps
is not nullable and an explicit boolean
is provided to indicate whether it's the initial derivation or not.
Does the diff help to show what I mean about the boilerplate required when we have to "sync" to state?
Thanks
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.
Notice file 2 is using a slightly different signature to the proposed in my last response; I agree with @gaearon the nullability issue is best avoided, hence prevProps is not nullable and an explicit boolean is provided to indicate whether it's the initial derivation or not.
Previous props would still need to be null or undefined in this case, no? Or even more confusingly, they would equal current props.
Anyway~ thanks for the feedback, Paul. I understand and appreciate your concern about verbosity. 🙇
This proposal was discussed in length in December, accepted, and merged into the React repo a couple of weeks ago. I'm going to lock down the thread at this point because I think we've already committed to this API.
Hope you understand!
componentDidMount() { | ||
// 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 => { |
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.
this.props ?
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.
This PR was merged 9 days ago 😄 but that typo has been fixed in master.
Note for React Native users: if you see a warning pointing to this page, this was most likely due to a mistake in our release process. This warning wasn’t supposed to fire until we finish writing the documentation for it, and wasn’t supposed to link to this page. We will either publish the documentation ASAP or revert this mistake in a patch release today (7th of March). We’re sorry for the disturbance.
If you reached this page from a warning saying "Legacy Context API has been detected within a strict mode tree.", you should visit this page for details on the stable Context API.
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.
Note that I plan to submit a related RFC soon for a new server-side lifecycle,
componentDidServerRender
, to offset any server-rendering functionality lost by deprecatingcomponentWillMount
.Please leave comments below for high-level topics. For feedback on specific parts of this proposal, please leave inline comments (on the markdown file).
View proposal with formatting