Streaming with React Suspense allows apps to speed up initial renders by unblocking initial UI by deferring non-critical data.
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>;
}