Streaming with React Suspense allows apps to speed up initial renders by deferring non-critical data and unblocking UI rendering.
React Router supports React Suspense by returning promises from loaders and actions.
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.
import type { Route } from "./+types/my-route";
export async function loader({}: Route.LoaderArgs) {
// note this is NOT awaited
let nonCriticalData = new Promise((res) =>
setTimeout(() => "non-critical", 5000)
);
let criticalData = await new Promise((res) =>
setTimeout(() => "critical", 300)
);
return { nonCriticalData, criticalData };
}
The promise will be available on loaderData
, <Await>
will await the promise and trigger <Suspense>
to render the fallback UI.
import * as React from "react";
import { Await } from "react-router";
// [previous code]
export default function MyComponent({
loaderData,
}: Route.ComponentProps) {
let { criticalData, nonCriticalData } = loaderData;
return (
<div>
<h1>Streaming example</h1>
<h2>Critical data value: {criticalData}</h2>
<React.Suspense fallback={<div>Loading...</div>}>
<Await resolve={nonCriticalData}>
{(value) => <h3>Non critical value: {value}</h3>}
</Await>
<NonCriticalUI p={nonCriticalData} />
</React.Suspense>
</div>
);
}
If you're experimenting with 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.
<React.Suspense fallback={<div>Loading...</div>}>
<NonCriticalUI p={nonCriticalData} />
</React.Suspense>
function NonCriticalUI({ p }: { p: Promise<string> }) {
let value = React.use(p);
return <h3>Non critical value {value}</h3>;
}