React Router 17 ⚛️ Deferred Data — Streaming slow requests
The previous chapters covered the data loaders, which fetch the data before the route renders, and the pending states, which show the loading UI while the fetch is in progress. This chapter introduces the deferred data: the mechanism that splits the loader into the critical data (awaited) and the slow data (streamed). The default loader pattern blocks the render until all the data is ready. The deferred pattern renders the critical data immediately, and the slow data arrives later with the <Suspense> fallback. The critical distinction is between the await and the defer(). The await blocks. The defer() returns a promise that the router streams to the client. The <Await> component (or the useLoaderData with the <Suspense>) unwraps the promise on the client.
Key point: The defer() function in the loader returns the object with the critical data (the plain values) and the slow data (the promises). The router sends the critical data in the initial HTML, and the promises stream later. The component reads the critical data with the useLoaderData, and the slow data with the <Await resolve={promise}> or the useAsyncValue. The <Suspense> wraps the <Await> with the fallback. The deferred data is for the slow, non-critical data: the comments, the recommendations, the analytics. The critical data must be awaited.
Why deferred data requires a specific mental model
The blocking problem. The default loader fetches the data sequentially or in parallel and returns when all the data is ready. The page renders after the slowest fetch. The user waits for the slow data even if the critical data is fast. The deferred data renders the critical data immediately and the slow data as it arrives.
The defer() problem. The defer({ critical: value, slow: promise }) returns the object. The critical is the plain value. The slow is the promise. The router serializes the critical to the initial HTML. The promise is the placeholder that the router streams later. The component reads the critical immediately. The slow is the promise that the <Await> unwraps.
The <Await> problem. The <Await resolve={slow}> is the component that unwraps the promise. The children is the render function that receives the resolved value. The <Suspense> wraps the <Await> with the fallback. The fallback is the loading state for the slow data. The critical data is already rendered.
The useAsyncValue problem. The useAsyncValue() is the hook that reads the resolved value from the nearest <Await>. The component inside the <Await> uses the hook instead of the children function. The two patterns are equivalent.
The error problem. The slow promise can reject. The <Await> has the errorElement prop. The error is the render failure for the slow data. The critical data is already rendered. The error boundary is the local.
The trade-off. The deferred data is the optimization for the slow, non-critical data. The critical data is the blocking. The overuse of the defer makes the page fragmented and the hydration complex. The correct use is the specific slow data that should not block the page.
a. The defer() in the loader
The loader returns the defer() with the critical and the slow data.
import { defer } from "react-router";
async function loader({ params }) {
const postPromise = fetchPost(params.id);
const commentsPromise = fetchComments(params.id);
return defer({
post: await postPromise,
comments: commentsPromise,
});
}
The post is the awaited, the critical. The comments is the unawaited, the slow. The await postPromise blocks the critical fetch. The commentsPromise is the promise that the router streams.
The component reads the critical with the useLoaderData.
import { useLoaderData } from "react-router";
function PostPage() {
const { post } = useLoaderData();
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
}
The post is the plain object. The component renders immediately. The comments are not in the useLoaderData.
b. The <Await> and the <Suspense>
The <Await> unwraps the slow promise. The <Suspense> is the fallback.
import { Await, useLoaderData } from "react-router";
import { Suspense } from "react";
function PostPage() {
const { post, comments } = useLoaderData();
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
<Suspense fallback={<p>Loading comments...</p>}>
<Await resolve={comments} errorElement={<p>Failed to load comments</p>}>
{(comments) => (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.text}</li>
))}
</ul>
)}
</Await>
</Suspense>
</div>
);
}
The <Suspense> shows the loading state while the promise resolves. The <Await> resolves the promise and passes the value to the children function. The errorElement handles the rejection. The critical post is already rendered.
The useAsyncValue alternative.
import { Await, useAsyncValue } from "react-router";
function Comments() {
const comments = useAsyncValue();
return (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.text}</li>
))}
</ul>
);
}
function PostPage() {
const { post, comments } = useLoaderData();
return (
<div>
<h1>{post.title}</h1>
<Suspense fallback={<p>Loading...</p>}>
<Await resolve={comments}>
<Comments />
</Await>
</Suspense>
</div>
);
}
The Comments component reads the value with the useAsyncValue. The Await wraps the component. The result is the same.
c. The SSR and the streaming
The deferred data is the most useful in the SSR, where the critical data is in the initial HTML and the slow data streams later.
// server.tsx
import { renderToPipeableStream } from "react-dom/server";
function handleRequest(req, res) {
const stream = renderToPipeableStream(<RouterProvider router={router} />, {
onShellReady() {
res.statusCode = 200;
res.setHeader("Content-Type", "text/html");
stream.pipe(res);
},
});
}
The renderToPipeableStream streams the HTML as the components render. The critical data is in the initial chunk. The slow data resolves later, and the HTML is patched by the client’s JavaScript. The user sees the critical content immediately and the slow content when it arrives.
The <Suspense> boundaries are the streaming points. Each <Suspense> is a chunk that the server can flush independently.
<Suspense fallback={<Spinner />}>
<Await resolve={comments}>
{(comments) => <Comments comments={comments} />}
</Await>
</Suspense>
The Suspense boundary is the chunk boundary. The server sends the chunk when the boundary resolves. The client hydrates the chunk as it arrives.
The <script> to rehydrate.
<script>
window.__REACT_ROUTER_STREAM_DATA__ = {/* the deferred data */};
</script>
The router serializes the deferred promises into the stream. The client reads them from the global. The <Await> resolves them on the client.
Complete Example Session
# ============================================
# PART 1: DEFER IN THE LOADER
# ============================================
async function loader({ params }) {
const postPromise = fetchPost(params.id);
const commentsPromise = fetchComments(params.id);
return defer({
post: await postPromise,
comments: commentsPromise,
});
}
# ============================================
# PART 2: READ THE CRITICAL
# ============================================
const { post } = useLoaderData();
# ============================================
# PART 3: AWAIT THE SLOW
# ============================================
<Suspense fallback={<p>Loading...</p>}>
<Await resolve={comments}>
{(comments) => <Comments comments={comments} />}
</Await>
</Suspense>
# ============================================
# PART 4: ERROR HANDLING
# ============================================
<Await resolve={comments} errorElement={<p>Failed</p>}>
# ============================================
# PART 5: USE ASYNC VALUE
# ============================================
function Comments() {
const comments = useAsyncValue();
return <ul>{comments.map(...)}</ul>;
}
# ============================================
# PART 6: SUSPENSE BOUNDARIES
# ============================================
<Suspense fallback={<SidebarSkeleton />}>
<Await resolve={sidebar}>{/* ... */}</Await>
</Suspense>
# ============================================
# PART 7: SSR STREAM
# ============================================
renderToPipeableStream(<RouterProvider router={router} />, { onShellReady: () => stream.pipe(res) })
# ============================================
# PART 8: SERIALIZED DATA
# ============================================
<script>window.__REACT_ROUTER_STREAM_DATA__ = {...}</script>
# ============================================
# PART 9: VERIFY
# ============================================
# Post renders immediately → Comments load later → No blocking.
The nine parts trace the deferred data: the defer, the critical read, the await, the error, the async value, the suspense boundaries, the SSR stream, the serialized data, and the verification.
Quick Reference
Defer Functions
| Function | Purpose |
|---|---|
defer({...}) | Return the deferred object |
await promise | Block the critical data |
Hooks
| Hook | Purpose |
|---|---|
useLoaderData | Critical data |
useAsyncValue | Slow data value |
Components
| Component | Purpose |
|---|---|
<Await> | Resolve the promise |
<Suspense> | Show the fallback |
Error Handling
| Prop | Purpose |
|---|---|
errorElement on <Await> | Handle the rejection |
fallback on <Suspense> | Handle the loading |
Best Practices
✅ Do This:
return defer({ post: await postPromise, comments: commentsPromise });
<!-- Await the critical, defer the slow --> # ✅
<Suspense fallback={<Spinner />}>
<!-- Wrap the Await in the Suspense --> # ✅
<Await resolve={comments} errorElement={<Failed />}>
<!-- Handle the slow data error --> # ✅
❌ Don’t Do This:
# Defer the critical data
<!-- The page renders empty --> # ❌
# Use the Await without the Suspense
<!-- The promise resolution is not handled --> # ❌
# Defer everything
<!-- The page is fragmented --> # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Critical data not rendering | Deferred it | Await the critical |
| Slow data not showing | No <Await> | Wrap in the <Await> |
| Loading not showing | No <Suspense> | Wrap in the <Suspense> |
| Error not handled | No errorElement | Add the error element |
| Hydration mismatch | SSR/CSR mismatch | Use the serialized data |
Real-World Examples
1. Defer
return defer({ post: await fetchPost(id), comments: fetchComments(id) });
2. Critical Read
const { post } = useLoaderData();
3. Await
<Await resolve={comments}>{(c) => <Comments data={c} />}</Await>
4. Suspense
<Suspense fallback={<Spinner />}>...</Suspense>
5. Error Element
<Await resolve={comments} errorElement={<Failed />}>
6. Use Async Value
const comments = useAsyncValue();
7. Multiple Deferred
return defer({ post: await post, comments: fetchComments(id), related: fetchRelated(id) });
8. Streaming SSR
renderToPipeableStream(<RouterProvider router={router} />, { onShellReady: () => stream.pipe(res) })
9. Serialized Data
<script>window.__REACT_ROUTER_STREAM_DATA__ = {...}</script>
10. Nested Suspense
<Suspense><Suspense>...</Suspense></Suspense>
Visual
Default Loader Flow
┌──────────────────────────────────────────────┐
│ Loader runs │
│ → Await all data │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Component renders │
│ → All data ready │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ User sees the complete page │
│ → Wait for the slowest data │
└──────────────────────────────────────────────┘
Deferred Flow
┌──────────────────────────────────────────────┐
│ Loader runs │
│ → Await the critical │
│ → Defer the slow │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Critical renders immediately │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Slow promise resolves │
│ → The Await renders the slow data │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Defer function | defer({...}) |
| Critical | await |
| Slow | Promise |
| Await component | <Await resolve={promise}> |
| Fallback | <Suspense fallback={...}> |
| Slow data hook | useAsyncValue() |
| Error | errorElement on <Await> |
Key takeaways:
- The
defer()splits the loader. The critical data is awaited and renders immediately. The slow data is the promise that streams later. The user sees the critical content without waiting. - The
<Await>unwraps the slow promise. The<Suspense>is the fallback. TheuseAsyncValuereads the value. The combination is the deferred pattern. - The
errorElementon the<Await>handles the rejection. The slow promise can fail. The error is the local, not the whole route. The critical data is unaffected. - The deferred data is for the slow, non-critical. The comments, the recommendations, the analytics. The critical data (the post, the title, the main content) must be awaited.
- The SSR is the most useful. The
renderToPipeableStreamstreams the HTML. The critical data is in the initial chunk. The slow data resolves later. The user sees the content immediately.
Remember: The defer is the split. The await is the critical. The promise is the slow. The <Await> is the unwrap. The <Suspense> is the fallback. The useAsyncValue is the read. The exam tests the defer pattern and the Suspense boundary.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!