UNPKG

4.06 kB Markdown View Raw
1---
2title: Streaming with Suspense
3---
4
5# Streaming with Suspense
6
7[MODES: framework, data]
8
9<br/>
10<br/>
11
12Streaming with React Suspense allows apps to speed up initial renders by deferring non-critical data and unblocking UI rendering.
13
14React Router supports React Suspense by returning promises from loaders and actions.
15
16## 1. Return a promise from loader
17
18React Router awaits route loaders before rendering route components. To unblock the loader for non-critical data, return the promise instead of awaiting it in the loader.
19
20```tsx
21import type { Route } from "./+types/my-route";
22
23export async function loader({}: Route.LoaderArgs) {
24 // note this is NOT awaited
25 let nonCriticalData = new Promise((res) =>
26 setTimeout(() => res("non-critical"), 5000),
27 );
28
29 let criticalData = await new Promise((res) =>
30 setTimeout(() => res("critical"), 300),
31 );
32
33 return { nonCriticalData, criticalData };
34}
35```
36
37Note you can't return a single promise, it must be an object with keys.
38
39## 2. Render the fallback and resolved UI
40
41The promise will be available on `loaderData`, `<Await>` will await the promise and trigger `<Suspense>` to render the fallback UI.
42
43```tsx
44import * as React from "react";
45import { Await } from "react-router";
46
47// [previous code]
48
49export default function MyComponent({
50 loaderData,
51}: Route.ComponentProps) {
52 let { criticalData, nonCriticalData } = loaderData;
53
54 return (
55 <div>
56 <h1>Streaming example</h1>
57 <h2>Critical data value: {criticalData}</h2>
58
59 <React.Suspense fallback={<div>Loading...</div>}>
60 <Await resolve={nonCriticalData}>
61 {(value) => <h3>Non critical value: {value}</h3>}
62 </Await>
63 </React.Suspense>
64 </div>
65 );
66}
67```
68
69## With React 19
70
71If you're using React 19, you can use `React.use` instead of `Await`, but you'll need to create a new component and pass the promise down to trigger the suspense fallback.
72
73```tsx
74<React.Suspense fallback={<div>Loading...</div>}>
75 <NonCriticalUI p={nonCriticalData} />
76</React.Suspense>
77```
78
79```tsx
80function NonCriticalUI({ p }: { p: Promise<string> }) {
81 let value = React.use(p);
82 return <h3>Non critical value {value}</h3>;
83}
84```
85
86## Timeouts
87
88By default, loaders and actions reject any outstanding promises after 4950ms. You can control this by exporting a `streamTimeout` numerical value from your `entry.server.tsx`.
89
90```ts filename=entry.server.tsx
91// Reject all pending promises from handler functions after 10 seconds
92export const streamTimeout = 10_000;
93```
94
95## Handling early rejections (Node)
96
97React Router waits for all loaders to settle (via `Promise.all`) before it begins streaming the response. Once streaming has started, React Router catches subsequent rejections of your streamed promises and surfaces them to your `<Await>` (or React 19 `React.use`) error UI.
98
99However, if a streamed promise rejects _before_ all of the route's loaders have settled, React Router has not yet been able to attach a handler to it. In Node, an unhandled promise rejection will crash the process unless you have a top-level handler registered.
100
101For example, this can happen if a parent route's loader takes longer to resolve than a child route's streamed promise takes to reject:
102
103```tsx
104// parent.tsx — slow loader
105export async function loader() {
106 await new Promise((r) => setTimeout(r, 1000));
107 return { parent: "data" };
108}
109
110// child.tsx — fast-rejecting streamed promise
111export async function loader() {
112 let lazy = new Promise((_, reject) =>
113 setTimeout(() => reject(new Error("boom")), 100),
114 );
115 return { lazy };
116}
117```
118
119When `lazy` rejects before the parent loader resolves, the rejection bubbles to the node process as an unhandled rejection, which will crash the process without a user-defined handler.
120
121To prevent this, register a process-level `unhandledRejection` handler in your server entry:
122
123```ts filename=entry.server.ts
124process.on("unhandledRejection", (reason, promise) => {
125 console.error(
126 "Unhandled Rejection at:",
127 promise,
128 "reason:",
129 reason,
130 );
131});
132```
133
\No newline at end of file