-
Notifications
You must be signed in to change notification settings - Fork 47.4k
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
Add Postpone API #27238
Add Postpone API #27238
Conversation
Serialize as P. In dev we include reason and stack. Emit onPostpone event if it happens with the reason.
Deserialize into the same shape as postpone(). Could actually call it potentially.
This just uses a fake infinite promise as a short cut. We could be more clever in the code here and not create any listeners at all.
We just don't use native classes for this and brand check with $$typeof.
8c4c2fa
to
dbcc12f
Compare
cca7c85
to
6b31297
Compare
This is semantically the same as just throwing an error. It just triggers client rendering. The difference is in how it gets logged. On the server we log to onPostpone instead of onError. On the client this doesn't trigger a recoverable error. It's just silent since it was intentional.
@@ -2859,27 +2860,32 @@ function updateDehydratedSuspenseComponent( | |||
// This boundary is in a permanent fallback state. In this case, we'll never | |||
// get an update and we'll never be able to hydrate the final content. Let's just try the | |||
// client side render instead. | |||
let digest, message, stack; | |||
let digest: ?string; |
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 hit a weird Flow error that only repro:ed on CI and not locally. I thought they were supposed to be the same version. It considers it empty even though it's always initialized - unless I annotate.
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.
Have some nits and questions but seems good to land assuming the task status thing is not actually a problem
} else if (enablePostpone && x.$$typeof === REACT_POSTPONE_TYPE) { | ||
const postponeInstance: Postpone = (x: any); | ||
logPostpone(request, postponeInstance.message); | ||
emitPostponeChunk(request, task.id, postponeInstance); |
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 the task supposed to be in a new status here? I don't see why it would get retried but if it did it would attempt to resolve the model again.
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.
yea, that's right and it should probably be removed from the abortable tasks list so it doesn't get aborted later.
// TODO: Figure out a better signal than a magic digest value. | ||
errorDigest = 'POSTPONE'; |
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's maybe not much better but we could have non-postpone digest be null or non-empty string and then empty string digest can represent postpones. This means collisions are impossible. The questions is really whether normal errors without a digest or postpones are more common, whichever is should be the one that omits the digest value and the other can be represented by an empty string
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 think postpones should be more common. E.g. triggering intentional client rendering would happen every time. Where as errors really shouldn't happen at all ideally.
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.
Well, bugs are probably more common but when that happens you're down the path of a bad experience anyway.
// These errors should never make it into a build so we don't need to encode them in codes.json | ||
// eslint-disable-next-line react-internal/prod-error-codes | ||
throw new Error( | ||
'resolveErrorProd should never be called in development mode. Use resolveErrorDev instead. This is a bug in React.', |
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.
error message has wrong method name
// These errors should never make it into a build so we don't need to encode them in codes.json | ||
// eslint-disable-next-line react-internal/prod-error-codes | ||
throw new Error( | ||
'resolveErrorDev should never be called in production mode. Use resolveErrorProd instead. This is a bug in React.', |
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.
fix method name in error message
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 intentionally did this because I hate taking numbers away from the codes.json for these internal things that shouldn't happen and if I just inlined this wouldn't be necessary. :)
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 guess these are not in the codes file so maybe that's fine.
if (value !== null && typeof value === 'object') { | ||
if (enablePostpone && value.$$typeof === REACT_POSTPONE_TYPE) { | ||
// Act as if this is an infinitely suspending promise. | ||
value = {then: function () {}}; |
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.
TIL our implementation of thenable does not assume the result is also thenable
This adds an experimental `unstable_postpone(reason)` API. Currently we don't have a way to model effectively an Infinite Promise. I.e. something that suspends but never resolves. The reason this is useful is because you might have something else that unblocks it later. E.g. by updating in place later, or by client rendering. On the client this works to model as an Infinite Promise (in fact, that's what this implementation does). However, in Fizz and Flight that doesn't work because the stream needs to end at some point. We don't have any way of knowing that we're suspended on infinite promises. It's not enough to tag the promises because you could await those and thus creating new promises. The only way we really have to signal this through a series of indirections like async functions, is by throwing. It's not 100% safe because these values can be caught but it's the best we can do. Effectively `postpone(reason)` behaves like a built-in [Catch Boundary](#26854). It's like `raise(Postpone, reason)` except it's built-in so it needs to be able to be encoded and caught by Suspense boundaries. In Flight and Fizz these behave pretty much the same as errors. Flight just forwards it to retrigger on the client. In Fizz they just trigger client rendering which itself might just postpone again or fill in the value. The difference is how they get logged. In Flight and Fizz they log to `onPostpone(reason)` instead of `onError(error)`. This log is meant to help find deopts on the server like finding places where you fall back to client rendering. The reason that you pass in is for that purpose to help the reason for any deopts. I do track the stack trace in DEV but I don't currently expose it to `onPostpone`. This seems like a limitation. It might be better to expose the Postpone object which is an Error object but that's more of an implementation detail. I could also pass it as a second argument. On the client after hydration they don't get passed to `onRecoverableError`. There's no global `onPostpone` API to capture postponed things on the client just like there's no `onError`. At that point it's just assumed to be intentional. It doesn't have any `digest` or reason passed to the client since it's not logged. There are some hacky solutions that currently just tries to reuse as much of the existing code as possible but should be more properly implemented. - Fiber is currently just converting it to a fake Promise object so that it behaves like an infinite Promise. - Fizz is encoding the magic digest string `"POSTPONE"` in the HTML so we know to ignore it but it should probably just be something neater that doesn't share namespace with digests. Next I plan on using this in the `/static` entry points for additional features. Why "postpone"? It's basically a synonym to "defer" but we plan on using "defer" for other purposes and it's overloaded anyway. DiffTrain build for [ac1a16c](ac1a16c)
### React upstream changes - facebook/react#27265 - facebook/react#27259 - facebook/react#27264 - facebook/react#27257 - facebook/react#27258 - facebook/react#27187 - facebook/react#27243 - facebook/react#27205 - facebook/react#27220 - facebook/react#27238 - facebook/react#27234 - facebook/react#27224 - facebook/react#27223 - facebook/react#27222 This will help unblock #53906
This adds an experimental `unstable_postpone(reason)` API. Currently we don't have a way to model effectively an Infinite Promise. I.e. something that suspends but never resolves. The reason this is useful is because you might have something else that unblocks it later. E.g. by updating in place later, or by client rendering. On the client this works to model as an Infinite Promise (in fact, that's what this implementation does). However, in Fizz and Flight that doesn't work because the stream needs to end at some point. We don't have any way of knowing that we're suspended on infinite promises. It's not enough to tag the promises because you could await those and thus creating new promises. The only way we really have to signal this through a series of indirections like async functions, is by throwing. It's not 100% safe because these values can be caught but it's the best we can do. Effectively `postpone(reason)` behaves like a built-in [Catch Boundary](facebook#26854). It's like `raise(Postpone, reason)` except it's built-in so it needs to be able to be encoded and caught by Suspense boundaries. In Flight and Fizz these behave pretty much the same as errors. Flight just forwards it to retrigger on the client. In Fizz they just trigger client rendering which itself might just postpone again or fill in the value. The difference is how they get logged. In Flight and Fizz they log to `onPostpone(reason)` instead of `onError(error)`. This log is meant to help find deopts on the server like finding places where you fall back to client rendering. The reason that you pass in is for that purpose to help the reason for any deopts. I do track the stack trace in DEV but I don't currently expose it to `onPostpone`. This seems like a limitation. It might be better to expose the Postpone object which is an Error object but that's more of an implementation detail. I could also pass it as a second argument. On the client after hydration they don't get passed to `onRecoverableError`. There's no global `onPostpone` API to capture postponed things on the client just like there's no `onError`. At that point it's just assumed to be intentional. It doesn't have any `digest` or reason passed to the client since it's not logged. There are some hacky solutions that currently just tries to reuse as much of the existing code as possible but should be more properly implemented. - Fiber is currently just converting it to a fake Promise object so that it behaves like an infinite Promise. - Fizz is encoding the magic digest string `"POSTPONE"` in the HTML so we know to ignore it but it should probably just be something neater that doesn't share namespace with digests. Next I plan on using this in the `/static` entry points for additional features. Why "postpone"? It's basically a synonym to "defer" but we plan on using "defer" for other purposes and it's overloaded anyway.
This adds an experimental `unstable_postpone(reason)` API. Currently we don't have a way to model effectively an Infinite Promise. I.e. something that suspends but never resolves. The reason this is useful is because you might have something else that unblocks it later. E.g. by updating in place later, or by client rendering. On the client this works to model as an Infinite Promise (in fact, that's what this implementation does). However, in Fizz and Flight that doesn't work because the stream needs to end at some point. We don't have any way of knowing that we're suspended on infinite promises. It's not enough to tag the promises because you could await those and thus creating new promises. The only way we really have to signal this through a series of indirections like async functions, is by throwing. It's not 100% safe because these values can be caught but it's the best we can do. Effectively `postpone(reason)` behaves like a built-in [Catch Boundary](#26854). It's like `raise(Postpone, reason)` except it's built-in so it needs to be able to be encoded and caught by Suspense boundaries. In Flight and Fizz these behave pretty much the same as errors. Flight just forwards it to retrigger on the client. In Fizz they just trigger client rendering which itself might just postpone again or fill in the value. The difference is how they get logged. In Flight and Fizz they log to `onPostpone(reason)` instead of `onError(error)`. This log is meant to help find deopts on the server like finding places where you fall back to client rendering. The reason that you pass in is for that purpose to help the reason for any deopts. I do track the stack trace in DEV but I don't currently expose it to `onPostpone`. This seems like a limitation. It might be better to expose the Postpone object which is an Error object but that's more of an implementation detail. I could also pass it as a second argument. On the client after hydration they don't get passed to `onRecoverableError`. There's no global `onPostpone` API to capture postponed things on the client just like there's no `onError`. At that point it's just assumed to be intentional. It doesn't have any `digest` or reason passed to the client since it's not logged. There are some hacky solutions that currently just tries to reuse as much of the existing code as possible but should be more properly implemented. - Fiber is currently just converting it to a fake Promise object so that it behaves like an infinite Promise. - Fizz is encoding the magic digest string `"POSTPONE"` in the HTML so we know to ignore it but it should probably just be something neater that doesn't share namespace with digests. Next I plan on using this in the `/static` entry points for additional features. Why "postpone"? It's basically a synonym to "defer" but we plan on using "defer" for other purposes and it's overloaded anyway. DiffTrain build for commit ac1a16c.
This adds an experimental
unstable_postpone(reason)
API.Currently we don't have a way to model effectively an Infinite Promise. I.e. something that suspends but never resolves. The reason this is useful is because you might have something else that unblocks it later. E.g. by updating in place later, or by client rendering.
On the client this works to model as an Infinite Promise (in fact, that's what this implementation does). However, in Fizz and Flight that doesn't work because the stream needs to end at some point. We don't have any way of knowing that we're suspended on infinite promises. It's not enough to tag the promises because you could await those and thus creating new promises. The only way we really have to signal this through a series of indirections like async functions, is by throwing. It's not 100% safe because these values can be caught but it's the best we can do.
Effectively
postpone(reason)
behaves like a built-in Catch Boundary. It's likeraise(Postpone, reason)
except it's built-in so it needs to be able to be encoded and caught by Suspense boundaries.In Flight and Fizz these behave pretty much the same as errors. Flight just forwards it to retrigger on the client. In Fizz they just trigger client rendering which itself might just postpone again or fill in the value. The difference is how they get logged.
In Flight and Fizz they log to
onPostpone(reason)
instead ofonError(error)
. This log is meant to help find deopts on the server like finding places where you fall back to client rendering. The reason that you pass in is for that purpose to help the reason for any deopts.I do track the stack trace in DEV but I don't currently expose it to
onPostpone
. This seems like a limitation. It might be better to expose the Postpone object which is an Error object but that's more of an implementation detail. I could also pass it as a second argument.On the client after hydration they don't get passed to
onRecoverableError
. There's no globalonPostpone
API to capture postponed things on the client just like there's noonError
. At that point it's just assumed to be intentional. It doesn't have anydigest
or reason passed to the client since it's not logged.There are some hacky solutions that currently just tries to reuse as much of the existing code as possible but should be more properly implemented.
"POSTPONE"
in the HTML so we know to ignore it but it should probably just be something neater that doesn't share namespace with digests.Next I plan on using this in the
/static
entry points for additional features.Why "postpone"? It's basically a synonym to "defer" but we plan on using "defer" for other purposes and it's overloaded anyway.