From 20bd300b74f689da35df6dcf56ae99b93483a629 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 5 Dec 2024 16:48:18 -0500 Subject: [PATCH 1/3] Remove deprecated ABORT_DELAY in favor of streamTimeout --- .changeset/wild-dogs-double.md | 5 ++++ docs/explanation/special-files.md | 27 +++++++++++++++++++ integration/error-sanitization-test.ts | 10 ++----- integration/vite-dev-custom-entry-test.ts | 10 ++----- integration/vite-spa-mode-test.ts | 10 ++----- .../config/defaults/entry.server.node.tsx | 12 ++++----- packages/react-router/lib/dom/ssr/entry.ts | 1 - packages/react-router/lib/dom/ssr/server.tsx | 3 --- .../framework-express/app/entry.server.tsx | 18 +++---------- playground/framework/app/entry.server.tsx | 18 +++---------- 10 files changed, 51 insertions(+), 63 deletions(-) create mode 100644 .changeset/wild-dogs-double.md diff --git a/.changeset/wild-dogs-double.md b/.changeset/wild-dogs-double.md new file mode 100644 index 0000000000..d1d49f86f4 --- /dev/null +++ b/.changeset/wild-dogs-double.md @@ -0,0 +1,5 @@ +--- +"@react-router/dev": patch +--- + +Update default `entry.server.tsx` to use new `streamTimeout` value for Single Fetch diff --git a/docs/explanation/special-files.md b/docs/explanation/special-files.md index 3150907c7b..056e4b1d25 100644 --- a/docs/explanation/special-files.md +++ b/docs/explanation/special-files.md @@ -229,6 +229,32 @@ The `default` export of this module is a function that lets you create the respo This module should render the markup for the current page using a `` element with the `context` and `url` for the current request. This markup will (optionally) be re-hydrated once JavaScript loads in the browser using the [client entry module][client-entry]. +### `streamTimeout` + +If you are [streaming] responses, you can export an optional `streamTimeout` value (in milliseconds) that will control the amount of time the server will wait for streamed promises to resolve before rejecting them and closing the stream. + +It's recommended to decouple this value from the timeout in which you abort the React renderer - and you should always set the React timeout to a higher value so it has time to stream down the underlying rejections from your streamTimeout. + +```tsx +// Reject all pending promises from handler functions after 10 seconds +export const streamTimeout = 10000; + +export default function handleRequest(...) { + return new Promise((resolve, reject) => { + // ... + + const { pipe, abort } = renderToPipeableStream( + , + { /* ... */ } + ); + + // Abort the streaming render pass after 11 seconds soto allow the rejected + // boundaries to be flushed + setTimeout(abort, streamTimeout + 1000); + }); +} +``` + ### `handleDataRequest` You can export an optional `handleDataRequest` function that will allow you to modify the response of a data request. These are the requests that do not render HTML, but rather return the loader and action data to the browser once client-side hydration has occurred. @@ -289,3 +315,4 @@ Note that this does not handle thrown `Response` instances from your `loader`/`a [rendertopipeablestream]: https://react.dev/reference/react-dom/server/renderToPipeableStream [rendertoreadablestream]: https://react.dev/reference/react-dom/server/renderToReadableStream [node-streaming-entry-server]: https://github.com/remix-run/react-router/blob/dev/packages/react-router-dev/config/defaults/entry.server.node.tsx +[streaming]: ../how-to/suspense diff --git a/integration/error-sanitization-test.ts b/integration/error-sanitization-test.ts index e0614e428e..89048930cc 100644 --- a/integration/error-sanitization-test.ts +++ b/integration/error-sanitization-test.ts @@ -497,8 +497,6 @@ test.describe("Error Sanitization", () => { import { ServerRouter, isRouteErrorResponse } from "react-router"; import { renderToPipeableStream } from "react-dom/server"; - const ABORT_DELAY = 5_000; - export default function handleRequest( request, responseStatusCode, @@ -508,11 +506,7 @@ test.describe("Error Sanitization", () => { return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( - , + , { onShellReady() { shellRendered = true; @@ -545,7 +539,7 @@ test.describe("Error Sanitization", () => { } ); - setTimeout(abort, ABORT_DELAY); + setTimeout(abort, 5000); }); } diff --git a/integration/vite-dev-custom-entry-test.ts b/integration/vite-dev-custom-entry-test.ts index c9c05b9243..5236f51f56 100644 --- a/integration/vite-dev-custom-entry-test.ts +++ b/integration/vite-dev-custom-entry-test.ts @@ -14,8 +14,6 @@ const files: Files = async ({ port }) => ({ import { ServerRouter } from "react-router"; import { renderToPipeableStream } from "react-dom/server"; - const ABORT_DELAY = 5_000; - export default function handleRequest( request: Request, responseStatusCode: number, @@ -25,11 +23,7 @@ const files: Files = async ({ port }) => ({ return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( - , + , { onShellReady() { shellRendered = true; @@ -65,7 +59,7 @@ const files: Files = async ({ port }) => ({ } ); - setTimeout(abort, ABORT_DELAY); + setTimeout(abort, 5000); }); } `, diff --git a/integration/vite-spa-mode-test.ts b/integration/vite-spa-mode-test.ts index 74912c3444..be9c3238b3 100644 --- a/integration/vite-spa-mode-test.ts +++ b/integration/vite-spa-mode-test.ts @@ -296,8 +296,6 @@ test.describe("SPA Mode", () => { import { ServerRouter } from "react-router"; import { renderToPipeableStream } from "react-dom/server"; - const ABORT_DELAY = 5_000; - export default function handleRequest( request: Request, responseStatusCode: number, @@ -322,11 +320,7 @@ test.describe("SPA Mode", () => { const html = await new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( - , + , { onAllReady() { shellRendered = true; @@ -359,7 +353,7 @@ test.describe("SPA Mode", () => { } ); - setTimeout(abort, ABORT_DELAY); + setTimeout(abort, 5000); }); const shellHtml = fs diff --git a/packages/react-router-dev/config/defaults/entry.server.node.tsx b/packages/react-router-dev/config/defaults/entry.server.node.tsx index bffa8aa271..f907b4b41d 100644 --- a/packages/react-router-dev/config/defaults/entry.server.node.tsx +++ b/packages/react-router-dev/config/defaults/entry.server.node.tsx @@ -7,7 +7,7 @@ import { isbot } from "isbot"; import type { RenderToPipeableStreamOptions } from "react-dom/server"; import { renderToPipeableStream } from "react-dom/server"; -const ABORT_DELAY = 5_000; +export const streamTimeout = 5_000; export default function handleRequest( request: Request, @@ -28,11 +28,7 @@ export default function handleRequest( : "onShellReady"; const { pipe, abort } = renderToPipeableStream( - , + , { [readyOption]() { shellRendered = true; @@ -65,6 +61,8 @@ export default function handleRequest( } ); - setTimeout(abort, ABORT_DELAY); + // Abort the rendering stream after the `streamTimeout` so it has tine to + // flush down the rejected boundaries + setTimeout(abort, streamTimeout + 1000); }); } diff --git a/packages/react-router/lib/dom/ssr/entry.ts b/packages/react-router/lib/dom/ssr/entry.ts index bded38733d..18094245de 100644 --- a/packages/react-router/lib/dom/ssr/entry.ts +++ b/packages/react-router/lib/dom/ssr/entry.ts @@ -17,7 +17,6 @@ export interface FrameworkContextObject { serverHandoffString?: string; future: FutureConfig; isSpaMode: boolean; - abortDelay?: number; serializeError?(error: Error): SerializedError; renderMeta?: { didRenderScripts?: boolean; diff --git a/packages/react-router/lib/dom/ssr/server.tsx b/packages/react-router/lib/dom/ssr/server.tsx index 1d91e31801..92d8597b45 100644 --- a/packages/react-router/lib/dom/ssr/server.tsx +++ b/packages/react-router/lib/dom/ssr/server.tsx @@ -11,7 +11,6 @@ import { StreamTransfer } from "./single-fetch"; export interface ServerRouterProps { context: EntryContext; url: string | URL; - abortDelay?: number; nonce?: string; } @@ -25,7 +24,6 @@ export interface ServerRouterProps { export function ServerRouter({ context, url, - abortDelay, nonce, }: ServerRouterProps): ReactElement { if (typeof url === "string") { @@ -79,7 +77,6 @@ export function ServerRouter({ future: context.future, isSpaMode: context.isSpaMode, serializeError: context.serializeError, - abortDelay, renderMeta: context.renderMeta, }} > diff --git a/playground/framework-express/app/entry.server.tsx b/playground/framework-express/app/entry.server.tsx index c4c366d190..1a1cbe6a58 100644 --- a/playground/framework-express/app/entry.server.tsx +++ b/playground/framework-express/app/entry.server.tsx @@ -6,8 +6,6 @@ import { ServerRouter } from "react-router"; import { isbot } from "isbot"; import { renderToPipeableStream } from "react-dom/server"; -const ABORT_DELAY = 5_000; - export default function handleRequest( request: Request, responseStatusCode: number, @@ -39,11 +37,7 @@ function handleBotRequest( return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( - , + , { onAllReady() { shellRendered = true; @@ -76,7 +70,7 @@ function handleBotRequest( } ); - setTimeout(abort, ABORT_DELAY); + setTimeout(abort, 5000); }); } @@ -89,11 +83,7 @@ function handleBrowserRequest( return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( - , + , { onShellReady() { shellRendered = true; @@ -126,6 +116,6 @@ function handleBrowserRequest( } ); - setTimeout(abort, ABORT_DELAY); + setTimeout(abort, 5000); }); } diff --git a/playground/framework/app/entry.server.tsx b/playground/framework/app/entry.server.tsx index c4c366d190..1a1cbe6a58 100644 --- a/playground/framework/app/entry.server.tsx +++ b/playground/framework/app/entry.server.tsx @@ -6,8 +6,6 @@ import { ServerRouter } from "react-router"; import { isbot } from "isbot"; import { renderToPipeableStream } from "react-dom/server"; -const ABORT_DELAY = 5_000; - export default function handleRequest( request: Request, responseStatusCode: number, @@ -39,11 +37,7 @@ function handleBotRequest( return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( - , + , { onAllReady() { shellRendered = true; @@ -76,7 +70,7 @@ function handleBotRequest( } ); - setTimeout(abort, ABORT_DELAY); + setTimeout(abort, 5000); }); } @@ -89,11 +83,7 @@ function handleBrowserRequest( return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( - , + , { onShellReady() { shellRendered = true; @@ -126,6 +116,6 @@ function handleBrowserRequest( } ); - setTimeout(abort, ABORT_DELAY); + setTimeout(abort, 5000); }); } From 3a5387244c1ee90cf8ee7b6f4a9983004a6c1f42 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 6 Dec 2024 11:14:58 -0500 Subject: [PATCH 2/3] Update docs and comment --- docs/explanation/special-files.md | 6 +++--- .../react-router/lib/server-runtime/single-fetch.ts | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/explanation/special-files.md b/docs/explanation/special-files.md index 056e4b1d25..7274569b5b 100644 --- a/docs/explanation/special-files.md +++ b/docs/explanation/special-files.md @@ -231,11 +231,11 @@ This module should render the markup for the current page using a ` controller.abort(new Error("Server Timeout")), typeof streamTimeout === "number" ? streamTimeout : 4950 From 1cf17f5e2dc9769989fd5b8b41d4f2137c8b2a7e Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 6 Dec 2024 11:57:16 -0500 Subject: [PATCH 3/3] Update changeset --- .changeset/wild-dogs-double.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.changeset/wild-dogs-double.md b/.changeset/wild-dogs-double.md index d1d49f86f4..3987630ac1 100644 --- a/.changeset/wild-dogs-double.md +++ b/.changeset/wild-dogs-double.md @@ -2,4 +2,7 @@ "@react-router/dev": patch --- -Update default `entry.server.tsx` to use new `streamTimeout` value for Single Fetch +Remove the leftover/unused `abortDelay` prop from `ServerRouter` and update the default `entry.server.tsx` to use the new `streamTimeout` value for Single Fetch + +- The `abortDelay` functionality was removed in v7 as it was coupled to the `defer` implementation from Remix v2, but this removal of this prop was missed +- If you were still using this prop in your `entry.server` file, it's likely your app is not aborting streams as you would expect and you will need to adopt the new [`streamTimeout`](https://reactrouter.com/explanation/special-files#streamtimeout) value introduced with Single Fetch