Repository: vercel/next.js
Pull request: [dynamicIO] Do not use `React.unstable_postpone()`

dynamicIO does not rely on Postponing for any prerender reason. This is an experimental only API with no clear path to stabilization. To make dynamicIO supportable with stable React we should eliminate use of this API.

This change replaces the postpone value when aborting the resume of a prerender we want to complete even when it had a postponed state with the already existing BailoutToCSRError error subclass.

We already have plumbing in place to ignore this error on the client when hydrating which is exactly what we want to achieve here.

Changed files:
filename: packages/next/errors.json
patch: @@ -718,5 +718,6 @@
   "717": "Unsupported environment condition \"%s\" and react condition \"%s\". This is a bug in Next.js.",
   "718": "Invariant: projectDir is required for node runtime",
   "719": "Failed to get source map for '%s'. This is a bug in Next.js",
-  "720": "A client prerender store should not be used for a route handler."
+  "720": "A client prerender store should not be used for a route handler.",
+  "721": "Render in Browser"
 }

filename: packages/next/src/server/app-render/app-render.tsx
patch: @@ -125,7 +125,7 @@ import {
 import { getStackWithoutErrorMessage } from '../../lib/format-server-error'
 import {
   accessedDynamicData,
-  createPostponedAbortSignal,
+  createRenderInBrowserAbortSignal,
   formatDynamicAPIAccesses,
   isPrerenderInterruptedError,
   createDynamicTrackingState,
@@ -3423,7 +3423,7 @@ async function prerenderToStream(
             />,
             JSON.parse(JSON.stringify(postponed)),
             {
-              signal: createPostponedAbortSignal('static prerender resume'),
+              signal: createRenderInBrowserAbortSignal(),
               onError: htmlRendererErrorHandler,
               nonce,
             }
@@ -3660,7 +3660,7 @@ async function prerenderToStream(
             />,
             JSON.parse(JSON.stringify(postponed)),
             {
-              signal: createPostponedAbortSignal('static prerender resume'),
+              signal: createRenderInBrowserAbortSignal(),
               onError: htmlRendererErrorHandler,
               nonce,
             }

filename: packages/next/src/server/app-render/dynamic-rendering.ts
patch: @@ -42,6 +42,7 @@ import {
   OUTLET_BOUNDARY_NAME,
 } from '../../lib/metadata/metadata-constants'
 import { scheduleOnNextTick } from '../../lib/scheduler'
+import { BailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
 
 const hasPostpone = typeof React.unstable_postpone === 'function'
 
@@ -495,15 +496,9 @@ function assertPostpone() {
  * This is a bit of a hack to allow us to abort a render using a Postpone instance instead of an Error which changes React's
  * abort semantics slightly.
  */
-export function createPostponedAbortSignal(reason: string): AbortSignal {
-  assertPostpone()
+export function createRenderInBrowserAbortSignal(): AbortSignal {
   const controller = new AbortController()
-  // We get our hands on a postpone instance by calling postpone and catching the throw
-  try {
-    React.unstable_postpone(reason)
-  } catch (x: unknown) {
-    controller.abort(x)
-  }
+  controller.abort(new BailoutToCSRError('Render in Browser'))
   return controller.signal
 }