From 15ef532b8ee6c75100990e25e7b5689d5728e256 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Thu, 3 Sep 2026 11:23:11 +0200 Subject: [PATCH 1/2] [react-dom] Update server-rendering options (#75488) Co-authored-by: Claude Code (kimi-k3[1m]) --- types/react-dom/server.browser.d.ts | 2 +- types/react-dom/server.bun.d.ts | 10 +++++- types/react-dom/server.d.ts | 39 ++++++++++++++++++++---- types/react-dom/server.edge.d.ts | 2 +- types/react-dom/server.node.d.ts | 1 + types/react-dom/static.browser.d.ts | 2 +- types/react-dom/static.d.ts | 16 ++++++++-- types/react-dom/test/react-dom-tests.tsx | 37 +++++++++++++++++----- 8 files changed, 89 insertions(+), 20 deletions(-) diff --git a/types/react-dom/server.browser.d.ts b/types/react-dom/server.browser.d.ts index f2e3bc930b3298..a37a109ae5de90 100644 --- a/types/react-dom/server.browser.d.ts +++ b/types/react-dom/server.browser.d.ts @@ -1 +1 @@ -export { renderToReadableStream, renderToStaticMarkup, renderToString } from "./server"; +export { renderToReadableStream, renderToStaticMarkup, renderToString, resume, version } from "./server"; diff --git a/types/react-dom/server.bun.d.ts b/types/react-dom/server.bun.d.ts index f2e3bc930b3298..4c87019dde2691 100644 --- a/types/react-dom/server.bun.d.ts +++ b/types/react-dom/server.bun.d.ts @@ -1 +1,9 @@ -export { renderToReadableStream, renderToStaticMarkup, renderToString } from "./server"; +export { + renderToPipeableStream, + renderToReadableStream, + renderToStaticMarkup, + renderToString, + resume, + resumeToPipeableStream, + version, +} from "./server"; diff --git a/types/react-dom/server.d.ts b/types/react-dom/server.d.ts index 84312f9d6f4175..fca0eb683d71e7 100644 --- a/types/react-dom/server.d.ts +++ b/types/react-dom/server.d.ts @@ -31,6 +31,25 @@ export interface BootstrapScriptDescriptor { crossOrigin?: string | undefined; } +/** + * The header content passed to `onHeaders` by `renderToPipeableStream`. + * Other server rendering APIs pass a `Headers` instance instead. + * React passes an empty object when there are no resource hints to send. + */ +export interface HeadersDescriptor { + Link?: string; +} + +/** + * A nonce string, or an object with separate nonces for scripts and styles. + */ +export type NonceOption = + | string + | { + script?: string | undefined; + style?: string | undefined; + }; + /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap Import maps} */ @@ -62,7 +81,7 @@ export interface ReactImportMap { export interface RenderToPipeableStreamOptions { identifierPrefix?: string; namespaceURI?: string; - nonce?: string; + nonce?: NonceOption | undefined; bootstrapScriptContent?: string; bootstrapScripts?: Array; bootstrapModules?: Array; @@ -71,10 +90,10 @@ export interface RenderToPipeableStreamOptions { * Must be a positive integer if specified. * @default 2000 */ - headersLengthHint?: number | undefined; + maxHeadersLength?: number | undefined; importMap?: ReactImportMap | undefined; progressiveChunkSize?: number; - onHeaders?: ((headers: Headers) => void) | undefined; + onHeaders?: ((headers: HeadersDescriptor) => void) | undefined; onShellReady?: () => void; onShellError?: (error: unknown) => void; onAllReady?: () => void; @@ -125,7 +144,7 @@ export interface RenderToReadableStreamOptions { identifierPrefix?: string; importMap?: ReactImportMap | undefined; namespaceURI?: string; - nonce?: string; + nonce?: NonceOption | undefined; bootstrapScriptContent?: string; bootstrapScripts?: Array; bootstrapModules?: Array; @@ -134,7 +153,7 @@ export interface RenderToReadableStreamOptions { * Must be a positive integer if specified. * @default 2000 */ - headersLengthHint?: number | undefined; + maxHeadersLength?: number | undefined; progressiveChunkSize?: number; signal?: AbortSignal; onError?: (error: unknown, errorInfo: ErrorInfo) => string | void; @@ -158,6 +177,14 @@ export function renderToReadableStream( export { ResumeOptions }; +export interface ResumeToPipeableStreamOptions { + nonce?: NonceOption | undefined; + onShellReady?: (() => void) | undefined; + onShellError?: ((error: unknown) => void) | undefined; + onAllReady?: (() => void) | undefined; + onError?: ((error: unknown, errorInfo: ErrorInfo) => string | void) | undefined; +} + /** * @see {@link https://react.dev/reference/react-dom/server/resume `resume`` reference documentation} * @version 19.2 @@ -175,7 +202,7 @@ export function resume( export function resumeToPipeableStream( children: React.ReactNode, postponedState: PostponedState, - options?: ResumeOptions, + options?: ResumeToPipeableStreamOptions, ): Promise; export const version: string; diff --git a/types/react-dom/server.edge.d.ts b/types/react-dom/server.edge.d.ts index f21d7d2b5ef229..a37a109ae5de90 100644 --- a/types/react-dom/server.edge.d.ts +++ b/types/react-dom/server.edge.d.ts @@ -1 +1 @@ -export { renderToReadableStream, renderToStaticMarkup, renderToString, resume } from "./server"; +export { renderToReadableStream, renderToStaticMarkup, renderToString, resume, version } from "./server"; diff --git a/types/react-dom/server.node.d.ts b/types/react-dom/server.node.d.ts index eb777a990353d8..4c87019dde2691 100644 --- a/types/react-dom/server.node.d.ts +++ b/types/react-dom/server.node.d.ts @@ -5,4 +5,5 @@ export { renderToString, resume, resumeToPipeableStream, + version, } from "./server"; diff --git a/types/react-dom/static.browser.d.ts b/types/react-dom/static.browser.d.ts index cd2d05ff521081..94c5393729fd34 100644 --- a/types/react-dom/static.browser.d.ts +++ b/types/react-dom/static.browser.d.ts @@ -1 +1 @@ -export { prerender, version } from "./static"; +export { prerender, resumeAndPrerender, version } from "./static"; diff --git a/types/react-dom/static.d.ts b/types/react-dom/static.d.ts index c86d5c10e6728c..902a0f4b82e9a4 100644 --- a/types/react-dom/static.d.ts +++ b/types/react-dom/static.d.ts @@ -72,6 +72,16 @@ export interface BootstrapScriptDescriptor { crossOrigin?: string | undefined; } +/** + * A nonce string, or an object with separate nonces for scripts and styles. + */ +export type NonceOption = + | string + | { + script?: string | undefined; + style?: string | undefined; + }; + export interface PrerenderOptions { bootstrapScriptContent?: string; bootstrapScripts?: Array; @@ -81,12 +91,12 @@ export interface PrerenderOptions { * Must be a positive integer if specified. * @default 2000 */ - headersLengthHint?: number | undefined; + maxHeadersLength?: number | undefined; identifierPrefix?: string; importMap?: ReactImportMap | undefined; namespaceURI?: string; onError?: (error: unknown, errorInfo: ErrorInfo) => string | void; - onHeaders?: (headers: Headers) => void | undefined; + onHeaders?: ((headers: Headers) => void) | undefined; progressiveChunkSize?: number; signal?: AbortSignal; } @@ -125,7 +135,7 @@ export function prerenderToNodeStream( ): Promise; export interface ResumeOptions { - nonce?: string; + nonce?: NonceOption | undefined; signal?: AbortSignal; onError?: (error: unknown) => string | undefined | void; } diff --git a/types/react-dom/test/react-dom-tests.tsx b/types/react-dom/test/react-dom-tests.tsx index 1184d2e084705e..2d84834c341e42 100644 --- a/types/react-dom/test/react-dom-tests.tsx +++ b/types/react-dom/test/react-dom-tests.tsx @@ -108,7 +108,7 @@ describe("ReactDOMStatic", () => { (await ReactDOMStatic.prerenderToNodeStream(React.createElement("div"))).prelude; ReactDOMStatic.prerenderToNodeStream(React.createElement("div"), { bootstrapScripts: ["./my-script.js"], - headersLengthHint: 4000, + maxHeadersLength: 4000, importMap, onHeaders(headers) { // $ExpectType Headers @@ -116,6 +116,29 @@ describe("ReactDOMStatic", () => { }, }); }); + + it("resumeToPipeableStream", async () => { + const { postponed } = await ReactDOMStatic.prerenderToNodeStream(children); + if (postponed !== null) { + ReactDOMServer.resumeToPipeableStream(children, postponed, { + nonce: { script: "script-nonce", style: "style-nonce" }, + onShellReady() {}, + onShellError(error) { + console.error(error); + }, + onAllReady() {}, + onError(error, errorInfo) { + // $ExpectType ErrorInfo + errorInfo; + return (error as { digest?: string | undefined }).digest; + }, + }); + ReactDOMServer.resumeToPipeableStream(children, postponed, { + // @ts-expect-error The Node.js stream variant does not support an abort signal + signal: new AbortController().signal, + }); + } + }); }); describe("React dom test utils", () => { @@ -254,10 +277,10 @@ function pipeableStreamDocumentedExample() { const response: Response = {} as any; const { pipe, abort } = ReactDOMServer.renderToPipeableStream(, { bootstrapScripts: ["/main.js"], - headersLengthHint: 4000, + maxHeadersLength: 4000, importMap, onHeaders(headers) { - // $ExpectType Headers + // $ExpectType HeadersDescriptor headers; }, onShellReady() { @@ -307,10 +330,10 @@ function pipeableStreamDocumentedStringExample() { const response: Response = {} as any; const { pipe, abort } = ReactDOMServer.renderToPipeableStream("app", { bootstrapScripts: ["/main.js"], - headersLengthHint: 4000, + maxHeadersLength: 4000, importMap, onHeaders(headers) { - // $ExpectType Headers + // $ExpectType HeadersDescriptor headers; }, onShellReady() { @@ -354,7 +377,7 @@ async function readableStreamDocumentedExample() { Success , { - headersLengthHint: 4000, + maxHeadersLength: 4000, importMap, signal: controller.signal, onError(error) { @@ -392,7 +415,7 @@ async function readableStreamDocumentedStringExample() { const stream = await ReactDOMServer.renderToReadableStream( "app", { - headersLengthHint: 4000, + maxHeadersLength: 4000, importMap, signal: controller.signal, onError(error) { From 8443606b19593c505271bda9ad1238e10a2ef78a Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Thu, 3 Sep 2026 12:36:43 +0200 Subject: [PATCH 2/2] [react-dom] Add `onBrowserBailout` (#75489) Co-authored-by: Claude Code (kimi-k3[1m]) --- types/react-dom/canary.d.ts | 45 +++++++++++++++++++++++++++ types/react-dom/test/canary-tests.tsx | 11 +++++++ 2 files changed, 56 insertions(+) diff --git a/types/react-dom/canary.d.ts b/types/react-dom/canary.d.ts index 31957fd8f5a487..c948464368570c 100644 --- a/types/react-dom/canary.d.ts +++ b/types/react-dom/canary.d.ts @@ -30,6 +30,7 @@ import React = require("react"); import ReactDOM = require("."); +import { ErrorInfo } from "./client"; export {}; @@ -50,6 +51,50 @@ declare module "." { function browser(reason?: string | (() => unknown)): BrowserUsable; } +declare module "./server" { + interface RenderToPipeableStreamOptions { + /** + * A callback React calls when it recovers from `browser()` by leaving a + * Suspense fallback for the browser to replace. + */ + onBrowserBailout?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined; + } + + interface RenderToReadableStreamOptions { + /** + * A callback React calls when it recovers from `browser()` by leaving a + * Suspense fallback for the browser to replace. + */ + onBrowserBailout?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined; + } + + interface ResumeToPipeableStreamOptions { + /** + * A callback React calls when it recovers from `browser()` by leaving a + * Suspense fallback for the browser to replace. + */ + onBrowserBailout?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined; + } +} + +declare module "./static" { + interface PrerenderOptions { + /** + * A callback React calls when it recovers from `browser()` by leaving a + * Suspense fallback for the browser to replace. + */ + onBrowserBailout?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined; + } + + interface ResumeOptions { + /** + * A callback React calls when it recovers from `browser()` by leaving a + * Suspense fallback for the browser to replace. + */ + onBrowserBailout?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined; + } +} + declare module "react" { interface RendererUsable { "react-dom/browser": BrowserUsable; diff --git a/types/react-dom/test/canary-tests.tsx b/types/react-dom/test/canary-tests.tsx index bdfd204d3c6934..19851100bcef0c 100644 --- a/types/react-dom/test/canary-tests.tsx +++ b/types/react-dom/test/canary-tests.tsx @@ -124,3 +124,14 @@ function browserUsableTests() { // @ts-expect-error -- the reason is a string or a zero-arg initializer, not an arbitrary value ReactDOM.browser(new Error("Only render this content in a browser")); } + +function browserBailoutTests() { + const onBrowserBailout = (error: unknown, errorInfo: ReactDOMClient.ErrorInfo) => {}; + + ReactDOMServer.renderToPipeableStream(React.createElement("div"), { onBrowserBailout }); + ReactDOMServer.renderToReadableStream(React.createElement("div"), { onBrowserBailout }); + ReactDOMServer.resume(React.createElement("div"), null as any, { onBrowserBailout }); + ReactDOMServer.resumeToPipeableStream(React.createElement("div"), null as any, { onBrowserBailout }); + ReactDOMStatic.prerender(React.createElement("div"), { onBrowserBailout }); + ReactDOMStatic.resumeAndPrerender(React.createElement("div"), null, { onBrowserBailout }); +}