UNPKG

4.95 kB Markdown View Raw
1---
2title: Streaming with Suspense
3---
4
5# Streaming with Suspense
6
7[MODES: framework, data]
8
9Streaming with React Suspense allows apps to speed up initial renders by deferring non-critical data and unblocking UI rendering.
10
11React Router supports React Suspense by returning promises from loaders and actions.
12
13## 1. Return a promise from loader
14
15React 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.
16
17```tsx
18import type { Route } from "./+types/my-route";
19
20export async function loader({}: Route.LoaderArgs) {
21 // note this is NOT awaited
22 let nonCriticalData = new Promise((res) =>
23 setTimeout(() => res("non-critical"), 5000),
24 );
25
26 let criticalData = await new Promise((res) =>
27 setTimeout(() => res("critical"), 300),
28 );
29
30 return { nonCriticalData, criticalData };
31}
32```
33
34Note you can't return a single promise, it must be an object with keys.
35
36## 2. Render the fallback and resolved UI
37
38The promise will be available on `loaderData`, `<Await>` will await the promise and trigger `<Suspense>` to render the fallback UI.
39
40```tsx
41import * as React from "react";
42import { Await } from "react-router";
43
44// [previous code]
45
46export default function MyComponent({
47 loaderData,
48}: Route.ComponentProps) {
49 let { criticalData, nonCriticalData } = loaderData;
50
51 return (
52 <div>
53 <h1>Streaming example</h1>
54 <h2>Critical data value: {criticalData}</h2>
55
56 <React.Suspense fallback={<div>Loading...</div>}>
57 <Await resolve={nonCriticalData}>
58 {(value) => <h3>Non critical value: {value}</h3>}
59 </Await>
60 </React.Suspense>
61 </div>
62 );
63}
64```
65
66## Opt into React's `use()` API
67
68<docs-warning>The `unstable_promise` prop is experimental and may change in minor or patch releases.</docs-warning>
69
70Use `<Await unstable_promise>` instead of `<Await resolve>` to opt into `React.use` under the hood while keeping the Await API:
71
72```tsx
73<React.Suspense fallback={<div>Loading...</div>}>
74 <Await
75 unstable_promise={nonCriticalData}
76 errorElement={<LoadingError />}
77 >
78 {(value) => <h3>Non critical value: {value}</h3>}
79 </Await>
80</React.Suspense>
81```
82
83This should largely behave the same as `<Await resolve>` while avoiding the legacy React Router promise tracking. Both support render-prop children, `useAsyncValue`, and `useAsyncError`. One notable difference with `unstable_promise` is that `errorElement` only handles promise rejections. Errors thrown while rendering children bubble to the nearest React error boundary.
84
85## Use React directly
86
87You can also read the promise directly with `React.use`. Pass it to a child component rendered inside `<React.Suspense>` so the boundary can display its fallback while the promise is pending.
88
89```tsx
90<React.Suspense fallback={<div>Loading...</div>}>
91 <NonCriticalUI p={nonCriticalData} />
92</React.Suspense>
93```
94
95```tsx
96function NonCriticalUI({ p }: { p: Promise<string> }) {
97 let value = React.use(p);
98 return <h3>Non critical value {value}</h3>;
99}
100```
101
102## Timeouts
103
104By 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`.
105
106```ts filename=entry.server.tsx
107// Reject all pending promises from handler functions after 10 seconds
108export const streamTimeout = 10_000;
109```
110
111## Handling early rejections (Node)
112
113React 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.use`) error UI.
114
115However, 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.
116
117For example, this can happen if a parent route's loader takes longer to resolve than a child route's streamed promise takes to reject:
118
119```tsx
120// parent.tsx — slow loader
121export async function loader() {
122 await new Promise((r) => setTimeout(r, 1000));
123 return { parent: "data" };
124}
125
126// child.tsx — fast-rejecting streamed promise
127export async function loader() {
128 let lazy = new Promise((_, reject) =>
129 setTimeout(() => reject(new Error("boom")), 100),
130 );
131 return { lazy };
132}
133```
134
135When `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.
136
137To prevent this, register a process-level `unhandledRejection` handler in your server entry:
138
139```ts filename=entry.server.ts
140process.on("unhandledRejection", (reason, promise) => {
141 console.error(
142 "Unhandled Rejection at:",
143 promise,
144 "reason:",
145 reason,
146 );
147});
148```
149
\No newline at end of file