Skip to content

feat(@react-router/dev): remove deprecated ABORT_DELAY in favor of streamTimeout #12478

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

Merged
merged 3 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/wild-dogs-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@react-router/dev": patch
---

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
27 changes: 27 additions & 0 deletions docs/explanation/special-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<ServerRouter>` 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 settle before rejecting outstanding promises them and closing the stream.

It's recommended to decouple this value from the timeout in which you abort the React renderer. You should always set the React rendering timeout to a higher value so it has time to stream down the underlying rejections from your `streamTimeout`.

```tsx lines=[1-2,13-15]
// 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(
<ServerRouter context={routerContext} url={request.url} />,
{ /* ... */ }
);

// 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.
Expand Down Expand Up @@ -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
10 changes: 2 additions & 8 deletions integration/error-sanitization-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -508,11 +506,7 @@ test.describe("Error Sanitization", () => {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<ServerRouter
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
<ServerRouter context={remixContext} url={request.url} />,
{
onShellReady() {
shellRendered = true;
Expand Down Expand Up @@ -545,7 +539,7 @@ test.describe("Error Sanitization", () => {
}
);

setTimeout(abort, ABORT_DELAY);
setTimeout(abort, 5000);
});
}

Expand Down
10 changes: 2 additions & 8 deletions integration/vite-dev-custom-entry-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,11 +23,7 @@ const files: Files = async ({ port }) => ({
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<ServerRouter
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
<ServerRouter context={remixContext} url={request.url} />,
{
onShellReady() {
shellRendered = true;
Expand Down Expand Up @@ -65,7 +59,7 @@ const files: Files = async ({ port }) => ({
}
);

setTimeout(abort, ABORT_DELAY);
setTimeout(abort, 5000);
});
}
`,
Expand Down
10 changes: 2 additions & 8 deletions integration/vite-spa-mode-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -322,11 +320,7 @@ test.describe("SPA Mode", () => {
const html = await new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<ServerRouter
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
<ServerRouter context={remixContext} url={request.url} />,
{
onAllReady() {
shellRendered = true;
Expand Down Expand Up @@ -359,7 +353,7 @@ test.describe("SPA Mode", () => {
}
);

setTimeout(abort, ABORT_DELAY);
setTimeout(abort, 5000);
});

const shellHtml = fs
Expand Down
12 changes: 5 additions & 7 deletions packages/react-router-dev/config/defaults/entry.server.node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,11 +28,7 @@ export default function handleRequest(
: "onShellReady";

const { pipe, abort } = renderToPipeableStream(
<ServerRouter
context={routerContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
<ServerRouter context={routerContext} url={request.url} />,
{
[readyOption]() {
shellRendered = true;
Expand Down Expand Up @@ -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);
});
}
1 change: 0 additions & 1 deletion packages/react-router/lib/dom/ssr/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export interface FrameworkContextObject {
serverHandoffString?: string;
future: FutureConfig;
isSpaMode: boolean;
abortDelay?: number;
serializeError?(error: Error): SerializedError;
renderMeta?: {
didRenderScripts?: boolean;
Expand Down
3 changes: 0 additions & 3 deletions packages/react-router/lib/dom/ssr/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { StreamTransfer } from "./single-fetch";
export interface ServerRouterProps {
context: EntryContext;
url: string | URL;
abortDelay?: number;
Copy link
Member

@MichaelDeBoey MichaelDeBoey Dec 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this prop would be a breaking change

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a warning for now that it's non-functional and deprecated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making it non-functional would be a breaking change as well

Deprecating it and pointing towards using streamTimeout would be a good idea though!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's already non-functional, though. At least based on the issue that spawned this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it's now a useless prop that was supposed to be removed in 7.0.0 but got missed. I think you can view it as a types bug fix since the type allows you to pass a thing that does nothing. FWIW it's also a build-time "break" not a runtime functional breaking change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed with the tam we feel this is a types bug fix.

Another advantage is the error TS will provide after this change is a good thing because if you are still passing abortDelay in RR v7 then it's highly likely you have a functional bug in your app because your streams are not going to timeout properly. Surfacing this TS proper error will alert you to fix that bug.

nonce?: string;
}

Expand All @@ -25,7 +24,6 @@ export interface ServerRouterProps {
export function ServerRouter({
context,
url,
abortDelay,
nonce,
}: ServerRouterProps): ReactElement {
if (typeof url === "string") {
Expand Down Expand Up @@ -79,7 +77,6 @@ export function ServerRouter({
future: context.future,
isSpaMode: context.isSpaMode,
serializeError: context.serializeError,
abortDelay,
renderMeta: context.renderMeta,
}}
>
Expand Down
11 changes: 6 additions & 5 deletions packages/react-router/lib/server-runtime/single-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,12 @@ export function encodeViaTurboStream(
) {
let controller = new AbortController();
// How long are we willing to wait for all of the promises in `data` to resolve
// before timing out? We default this to 50ms shorter than the default value for
// `ABORT_DELAY` in our built-in `entry.server.tsx` so that once we reject we
// have time to flush the rejections down through React's rendering stream before `
// we call abort() on that. If the user provides their own it's up to them to
// decouple the aborting of the stream from the aborting of React's renderToPipeableStream
// before timing out? We default this to 50ms shorter than the default value
// of 5000ms we had in `ABORT_DELAY` in Remix v2 that folks may still be using
// in RR v7 so that once we reject we have time to flush the rejections down
// through React's rendering stream before we call `abort()` on that. If the
// user provides their own it's up to them to decouple the aborting of the
// stream from the aborting of React's `renderToPipeableStream`
let timeoutId = setTimeout(
() => controller.abort(new Error("Server Timeout")),
typeof streamTimeout === "number" ? streamTimeout : 4950
Expand Down
18 changes: 4 additions & 14 deletions playground/framework-express/app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -39,11 +37,7 @@ function handleBotRequest(
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<ServerRouter
context={reactRouterContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
<ServerRouter context={reactRouterContext} url={request.url} />,
{
onAllReady() {
shellRendered = true;
Expand Down Expand Up @@ -76,7 +70,7 @@ function handleBotRequest(
}
);

setTimeout(abort, ABORT_DELAY);
setTimeout(abort, 5000);
});
}

Expand All @@ -89,11 +83,7 @@ function handleBrowserRequest(
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<ServerRouter
context={reactRouterContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
<ServerRouter context={reactRouterContext} url={request.url} />,
{
onShellReady() {
shellRendered = true;
Expand Down Expand Up @@ -126,6 +116,6 @@ function handleBrowserRequest(
}
);

setTimeout(abort, ABORT_DELAY);
setTimeout(abort, 5000);
});
}
18 changes: 4 additions & 14 deletions playground/framework/app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -39,11 +37,7 @@ function handleBotRequest(
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<ServerRouter
context={reactRouterContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
<ServerRouter context={reactRouterContext} url={request.url} />,
{
onAllReady() {
shellRendered = true;
Expand Down Expand Up @@ -76,7 +70,7 @@ function handleBotRequest(
}
);

setTimeout(abort, ABORT_DELAY);
setTimeout(abort, 5000);
});
}

Expand All @@ -89,11 +83,7 @@ function handleBrowserRequest(
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<ServerRouter
context={reactRouterContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
<ServerRouter context={reactRouterContext} url={request.url} />,
{
onShellReady() {
shellRendered = true;
Expand Down Expand Up @@ -126,6 +116,6 @@ function handleBrowserRequest(
}
);

setTimeout(abort, ABORT_DELAY);
setTimeout(abort, 5000);
});
}
Loading