| 1 | ---
|
| 2 | title: Streaming with Suspense
|
| 3 | ---
|
| 4 |
|
| 5 | # Streaming with Suspense
|
| 6 |
|
| 7 | [MODES: framework, data]
|
| 8 |
|
| 9 | Streaming with React Suspense allows apps to speed up initial renders by deferring non-critical data and unblocking UI rendering.
|
| 10 |
|
| 11 | React Router supports React Suspense by returning promises from loaders and actions.
|
| 12 |
|
| 13 | ## 1. Return a promise from loader
|
| 14 |
|
| 15 | React 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
|
| 18 | import type { Route } from "./+types/my-route";
|
| 19 |
|
| 20 | export 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 |
|
| 34 | Note you can't return a single promise, it must be an object with keys.
|
| 35 |
|
| 36 | ## 2. Render the fallback and resolved UI
|
| 37 |
|
| 38 | The promise will be available on `loaderData`, `<Await>` will await the promise and trigger `<Suspense>` to render the fallback UI.
|
| 39 |
|
| 40 | ```tsx
|
| 41 | import * as React from "react";
|
| 42 | import { Await } from "react-router";
|
| 43 |
|
| 44 | // [previous code]
|
| 45 |
|
| 46 | export 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 |
|
| 70 | Use `<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 |
|
| 83 | This 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 |
|
| 87 | You 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
|
| 96 | function 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 |
|
| 104 | By 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
|
| 108 | export const streamTimeout = 10_000;
|
| 109 | ```
|
| 110 |
|
| 111 | ## Handling early rejections (Node)
|
| 112 |
|
| 113 | React 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 |
|
| 115 | However, 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 |
|
| 117 | For 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
|
| 121 | export async function loader() {
|
| 122 | await new Promise((r) => setTimeout(r, 1000));
|
| 123 | return { parent: "data" };
|
| 124 | }
|
| 125 |
|
| 126 | // child.tsx — fast-rejecting streamed promise
|
| 127 | export async function loader() {
|
| 128 | let lazy = new Promise((_, reject) =>
|
| 129 | setTimeout(() => reject(new Error("boom")), 100),
|
| 130 | );
|
| 131 | return { lazy };
|
| 132 | }
|
| 133 | ```
|
| 134 |
|
| 135 | When `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 |
|
| 137 | To prevent this, register a process-level `unhandledRejection` handler in your server entry:
|
| 138 |
|
| 139 | ```ts filename=entry.server.ts
|
| 140 | process.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 |