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

Add Postpone API #27238

Merged
merged 10 commits into from
Aug 17, 2023
Merged

Add Postpone API #27238

merged 10 commits into from
Aug 17, 2023

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Aug 17, 2023

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 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.

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.
@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Aug 17, 2023
@sebmarkbage sebmarkbage force-pushed the postpone branch 2 times, most recently from 8c4c2fa to dbcc12f Compare August 17, 2023 02:02
@react-sizebot
Copy link

react-sizebot commented Aug 17, 2023

Comparing: ade82b8...bdd9703

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js = 164.32 kB 164.32 kB = 51.76 kB 51.76 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.04% 171.73 kB 171.81 kB +0.04% 53.98 kB 54.00 kB
facebook-www/ReactDOM-prod.classic.js +0.02% 567.12 kB 567.21 kB = 100.09 kB 100.10 kB
facebook-www/ReactDOM-prod.modern.js +0.02% 550.92 kB 551.00 kB = 97.25 kB 97.26 kB
oss-experimental/react-server/cjs/react-server-flight.development.js +3.04% 67.00 kB 69.04 kB +1.85% 16.16 kB 16.46 kB
oss-experimental/react-server/cjs/react-server-flight.production.min.js +2.90% 17.03 kB 17.53 kB +1.94% 6.07 kB 6.19 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.production.min.js +2.48% 11.28 kB 11.56 kB +1.21% 4.31 kB 4.36 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.production.min.js +2.47% 11.32 kB 11.60 kB +1.20% 4.34 kB 4.39 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.production.min.js +2.46% 11.42 kB 11.70 kB +1.03% 4.35 kB 4.40 kB
oss-experimental/react-server-dom-webpack/umd/react-server-dom-webpack-client.browser.production.min.js +2.42% 11.59 kB 11.87 kB +1.10% 4.44 kB 4.49 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.production.min.js +2.40% 11.68 kB 11.96 kB +1.14% 4.47 kB 4.52 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.production.min.js +2.34% 12.01 kB 12.29 kB +1.22% 4.60 kB 4.66 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.production.min.js +2.29% 12.25 kB 12.53 kB +1.22% 4.69 kB 4.74 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +2.16% 96.07 kB 98.15 kB +1.43% 22.93 kB 23.26 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js +2.08% 99.90 kB 101.98 kB +1.35% 24.13 kB 24.46 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.min.js +2.07% 24.54 kB 25.05 kB +1.46% 8.50 kB 8.62 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +2.07% 100.31 kB 102.39 kB +1.32% 24.26 kB 24.58 kB
oss-experimental/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.development.js +2.06% 105.58 kB 107.76 kB +1.24% 24.49 kB 24.80 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.min.js +2.03% 25.75 kB 26.27 kB +1.42% 8.76 kB 8.88 kB
oss-experimental/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.production.min.js +2.03% 25.94 kB 26.47 kB +1.52% 8.87 kB 9.00 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +2.01% 103.16 kB 105.24 kB +1.31% 24.58 kB 24.91 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.min.js +2.01% 26.06 kB 26.58 kB +1.40% 8.86 kB 8.98 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-server/cjs/react-server-flight.development.js +3.04% 67.00 kB 69.04 kB +1.85% 16.16 kB 16.46 kB
oss-experimental/react-server/cjs/react-server-flight.production.min.js +2.90% 17.03 kB 17.53 kB +1.94% 6.07 kB 6.19 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.production.min.js +2.48% 11.28 kB 11.56 kB +1.21% 4.31 kB 4.36 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.production.min.js +2.47% 11.32 kB 11.60 kB +1.20% 4.34 kB 4.39 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.production.min.js +2.46% 11.42 kB 11.70 kB +1.03% 4.35 kB 4.40 kB
oss-experimental/react-server-dom-webpack/umd/react-server-dom-webpack-client.browser.production.min.js +2.42% 11.59 kB 11.87 kB +1.10% 4.44 kB 4.49 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.production.min.js +2.40% 11.68 kB 11.96 kB +1.14% 4.47 kB 4.52 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.production.min.js +2.34% 12.01 kB 12.29 kB +1.22% 4.60 kB 4.66 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.production.min.js +2.29% 12.25 kB 12.53 kB +1.22% 4.69 kB 4.74 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +2.16% 96.07 kB 98.15 kB +1.43% 22.93 kB 23.26 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js +2.08% 99.90 kB 101.98 kB +1.35% 24.13 kB 24.46 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.min.js +2.07% 24.54 kB 25.05 kB +1.46% 8.50 kB 8.62 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +2.07% 100.31 kB 102.39 kB +1.32% 24.26 kB 24.58 kB
oss-experimental/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.development.js +2.06% 105.58 kB 107.76 kB +1.24% 24.49 kB 24.80 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.min.js +2.03% 25.75 kB 26.27 kB +1.42% 8.76 kB 8.88 kB
oss-experimental/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.production.min.js +2.03% 25.94 kB 26.47 kB +1.52% 8.87 kB 9.00 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +2.01% 103.16 kB 105.24 kB +1.31% 24.58 kB 24.91 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.min.js +2.01% 26.06 kB 26.58 kB +1.40% 8.86 kB 8.98 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +1.98% 105.21 kB 107.28 kB +1.29% 25.17 kB 25.49 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.production.min.js +1.89% 26.95 kB 27.46 kB +1.36% 9.17 kB 9.30 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.production.min.js +1.85% 27.48 kB 27.99 kB +1.34% 9.34 kB 9.46 kB
oss-experimental/react-server-dom-esm/esm/react-server-dom-esm-client.browser.production.min.js +1.78% 44.30 kB 45.09 kB +1.12% 10.85 kB 10.97 kB
oss-experimental/react-client/cjs/react-client-flight.production.min.js +1.69% 10.17 kB 10.34 kB +0.79% 3.91 kB 3.94 kB
oss-experimental/react-client/cjs/react-client-flight.development.js +1.43% 52.70 kB 53.46 kB +1.03% 13.00 kB 13.13 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.development.js +1.35% 55.92 kB 56.67 kB +1.03% 13.51 kB 13.65 kB
oss-experimental/react-server-dom-webpack/umd/react-server-dom-webpack-client.browser.development.js +1.34% 60.10 kB 60.91 kB +1.02% 13.88 kB 14.02 kB
oss-experimental/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js +1.34% 56.39 kB 57.14 kB +1.00% 13.57 kB 13.71 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js +1.33% 56.50 kB 57.26 kB +0.98% 13.66 kB 13.79 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.development.js +1.33% 56.55 kB 57.31 kB +1.01% 13.63 kB 13.77 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.development.js +1.32% 57.20 kB 57.95 kB +1.01% 13.83 kB 13.97 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.development.js +1.29% 58.44 kB 59.19 kB +0.99% 14.22 kB 14.36 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.development.js +1.27% 59.16 kB 59.91 kB +1.02% 14.37 kB 14.52 kB
oss-experimental/react/cjs/react.shared-subset.production.min.js +1.26% 8.36 kB 8.46 kB +0.87% 3.44 kB 3.47 kB
oss-experimental/react/cjs/react.production.min.js +1.16% 9.17 kB 9.28 kB +0.85% 3.42 kB 3.45 kB
oss-experimental/react-server/cjs/react-server.production.min.js +0.99% 25.46 kB 25.71 kB +1.21% 8.66 kB 8.77 kB
facebook-www/ReactDOMServer-prod.modern.js +0.80% 142.35 kB 143.50 kB +0.29% 26.15 kB 26.23 kB
oss-experimental/react/umd/react.profiling.min.js +0.77% 12.92 kB 13.01 kB +0.64% 4.96 kB 4.99 kB
oss-experimental/react/umd/react.production.min.js +0.77% 12.92 kB 13.02 kB +0.67% 4.96 kB 4.99 kB
facebook-www/ReactDOMServerStreaming-prod.modern.js +0.73% 149.03 kB 150.12 kB +0.28% 27.85 kB 27.93 kB
oss-experimental/react-server/cjs/react-server.development.js +0.70% 147.88 kB 148.91 kB +0.50% 36.86 kB 37.04 kB
oss-experimental/react-dom/cjs/react-dom-static.browser.production.min.js +0.43% 63.06 kB 63.33 kB +0.53% 19.71 kB 19.82 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.production.min.js +0.43% 63.18 kB 63.45 kB +0.46% 19.77 kB 19.86 kB
oss-experimental/react-dom/umd/react-dom-server.browser.production.min.js +0.43% 63.34 kB 63.61 kB +0.51% 20.04 kB 20.14 kB
oss-experimental/react-dom/cjs/react-dom-static.edge.production.min.js +0.43% 63.40 kB 63.67 kB +0.53% 19.83 kB 19.94 kB
oss-experimental/react-dom/umd/react-dom-server-legacy.browser.production.min.js +0.41% 61.75 kB 62.01 kB +0.46% 19.05 kB 19.14 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.production.min.js +0.41% 61.58 kB 61.84 kB +0.55% 18.67 kB 18.77 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.production.min.js +0.41% 66.22 kB 66.48 kB +0.55% 20.44 kB 20.55 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.production.min.js +0.40% 67.53 kB 67.80 kB +0.44% 21.24 kB 21.34 kB
oss-experimental/react-dom/cjs/react-dom-static.node.production.min.js +0.40% 67.60 kB 67.87 kB +0.43% 21.28 kB 21.37 kB
oss-experimental/react-dom/cjs/react-dom-server.node.production.min.js +0.40% 67.63 kB 67.89 kB +0.42% 21.26 kB 21.35 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.production.min.js +0.39% 66.47 kB 66.73 kB +0.55% 20.29 kB 20.40 kB
oss-experimental/react/cjs/react.shared-subset.development.js +0.36% 86.09 kB 86.40 kB +0.37% 23.92 kB 24.01 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.development.js +0.31% 344.92 kB 345.99 kB +0.25% 78.27 kB 78.46 kB
oss-experimental/react-dom/cjs/react-dom-static.browser.development.js +0.31% 346.42 kB 347.49 kB +0.25% 78.77 kB 78.96 kB
oss-experimental/react-dom/cjs/react-dom-static.edge.development.js +0.31% 346.83 kB 347.90 kB +0.24% 78.89 kB 79.08 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.development.js +0.31% 347.11 kB 348.19 kB +0.25% 78.95 kB 79.14 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.development.js +0.31% 347.52 kB 348.60 kB +0.24% 79.07 kB 79.26 kB
oss-experimental/react-dom/cjs/react-dom-static.node.development.js +0.31% 348.55 kB 349.63 kB +0.24% 79.09 kB 79.28 kB
oss-experimental/react-dom/cjs/react-dom-server.node.development.js +0.31% 348.59 kB 349.67 kB +0.24% 79.00 kB 79.19 kB
oss-experimental/react-dom/umd/react-dom-server.browser.development.js +0.31% 363.85 kB 364.97 kB +0.25% 79.84 kB 80.04 kB
oss-experimental/react/cjs/react.development.js +0.30% 101.98 kB 102.28 kB +0.28% 27.52 kB 27.60 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.development.js +0.30% 349.76 kB 350.81 kB +0.24% 79.31 kB 79.50 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.development.js +0.30% 347.94 kB 348.99 kB +0.24% 78.85 kB 79.04 kB
oss-experimental/react-dom/umd/react-dom-server-legacy.browser.development.js +0.30% 364.69 kB 365.78 kB +0.25% 79.72 kB 79.92 kB
oss-stable-semver/react-server/cjs/react-server-flight.development.js +0.30% 64.66 kB 64.85 kB +0.35% 15.78 kB 15.83 kB
oss-stable/react-server/cjs/react-server-flight.development.js +0.30% 64.66 kB 64.85 kB +0.35% 15.78 kB 15.83 kB
oss-stable-semver/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.min.js +0.28% 23.83 kB 23.89 kB +0.31% 8.32 kB 8.35 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.min.js +0.28% 23.83 kB 23.89 kB +0.31% 8.32 kB 8.35 kB
oss-stable-semver/react-server/cjs/react-server-flight.production.min.js +0.27% 16.36 kB 16.40 kB +0.27% 5.89 kB 5.91 kB
oss-stable/react-server/cjs/react-server-flight.production.min.js +0.27% 16.36 kB 16.40 kB +0.27% 5.89 kB 5.91 kB
oss-stable-semver/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.production.min.js +0.27% 25.19 kB 25.25 kB +0.22% 8.67 kB 8.69 kB
oss-stable/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.production.min.js +0.27% 25.19 kB 25.25 kB +0.22% 8.67 kB 8.69 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.min.js +0.26% 24.99 kB 25.06 kB +0.19% 8.57 kB 8.58 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.min.js +0.26% 24.99 kB 25.06 kB +0.19% 8.57 kB 8.58 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.min.js +0.26% 25.30 kB 25.36 kB +0.28% 8.66 kB 8.69 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.min.js +0.26% 25.30 kB 25.36 kB +0.28% 8.66 kB 8.69 kB
oss-experimental/react/umd/react.development.js +0.26% 125.20 kB 125.52 kB +0.21% 32.36 kB 32.43 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.production.min.js +0.25% 26.24 kB 26.30 kB +0.26% 9.00 kB 9.02 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.production.min.js +0.25% 26.24 kB 26.30 kB +0.26% 9.00 kB 9.02 kB
oss-stable-semver/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +0.25% 93.50 kB 93.73 kB +0.29% 22.43 kB 22.49 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +0.25% 93.50 kB 93.73 kB +0.29% 22.43 kB 22.49 kB
facebook-www/ReactDOMServer-prod.classic.js +0.25% 145.88 kB 146.24 kB +0.18% 26.83 kB 26.88 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.production.min.js +0.25% 26.77 kB 26.83 kB +0.21% 9.16 kB 9.18 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.production.min.js +0.25% 26.77 kB 26.83 kB +0.21% 9.16 kB 9.18 kB
oss-stable-semver/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.development.js +0.25% 102.41 kB 102.66 kB +0.37% 23.84 kB 23.93 kB
oss-stable/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.development.js +0.25% 102.41 kB 102.66 kB +0.37% 23.84 kB 23.93 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js +0.24% 96.88 kB 97.11 kB +0.30% 23.49 kB 23.57 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js +0.24% 96.88 kB 97.11 kB +0.30% 23.49 kB 23.57 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +0.24% 97.29 kB 97.52 kB +0.28% 23.61 kB 23.67 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +0.24% 97.29 kB 97.52 kB +0.28% 23.61 kB 23.67 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +0.23% 100.59 kB 100.82 kB +0.29% 24.08 kB 24.15 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +0.23% 100.59 kB 100.82 kB +0.29% 24.08 kB 24.15 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +0.23% 102.63 kB 102.87 kB +0.28% 24.69 kB 24.76 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +0.23% 102.63 kB 102.87 kB +0.28% 24.69 kB 24.76 kB

Generated by 🚫 dangerJS against bdd9703

@sebmarkbage sebmarkbage force-pushed the postpone branch 4 times, most recently from cca7c85 to 6b31297 Compare August 17, 2023 02:19
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;
Copy link
Collaborator Author

@sebmarkbage sebmarkbage Aug 17, 2023

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.

Copy link
Collaborator

@gnoff gnoff left a 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

Comment on lines 1385 to 1388
} else if (enablePostpone && x.$$typeof === REACT_POSTPONE_TYPE) {
const postponeInstance: Postpone = (x: any);
logPostpone(request, postponeInstance.message);
emitPostponeChunk(request, task.id, postponeInstance);
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Comment on lines +1718 to +1719
// TODO: Figure out a better signal than a magic digest value.
errorDigest = 'POSTPONE';
Copy link
Collaborator

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

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 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.

Copy link
Collaborator Author

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.',
Copy link
Collaborator

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.',
Copy link
Collaborator

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

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 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. :)

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 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 () {}};
Copy link
Collaborator

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

@sebmarkbage sebmarkbage merged commit ac1a16c into facebook:main Aug 17, 2023
github-actions bot pushed a commit that referenced this pull request Aug 17, 2023
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)
EdisonVan pushed a commit to EdisonVan/react that referenced this pull request Apr 15, 2024
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.
bigfootjon pushed a commit that referenced this pull request Apr 18, 2024
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants