Understanding React Router 6.4 Data APIs: Loader, Defer, and Await
This article explains how React Router 6.4’s new Data APIs—loader functions, defer, and the Await component—separate data fetching from rendering, improve client‑side and server‑side rendering performance, and provide a smoother user experience through progressive loading and streaming.
Introduction
React Router 6.4 introduced a series of Data APIs such as loaderFunction and defer , which decouple data fetching from page rendering and deliver a better user experience.
Why the Data APIs are Better
Traditional client‑side or server‑side rendering often blocks the UI while waiting for data, causing a waterfall effect. By triggering data requests as soon as a route is matched, the UI can render independently of the data.
Client‑Side Render
In a typical SPA, each component initiates its own data request during its mount lifecycle, leading to a waterfall of network calls and delayed rendering. React Router 6.4 introduces LoaderData , allowing the router to start data fetching before any component mounts, eliminating the waterfall.
Additionally, the defer API and the Await component enable selective postponement of parts of the UI, so non‑blocking data does not stall the entire page.
Server‑Side Render
SSR frameworks (e.g., Next.js, Nuxt.js) already separate data fetching from rendering, but they still block the response until all data resolves. Using defer together with streaming, React Router can send a partially rendered page (skeleton) while data continues to load, avoiding a white‑screen.
Quick Start
Below is a minimal example that demonstrates how to use the new Data APIs.
createBrowserRouter
Instead of the classic <BrowserRouter /> , you create a router with createBrowserRouter so that the routes can use Data APIs.
// Default data fetching
const getDeferredData = () => {
return new Promise(r => {
setTimeout(() => { r({ name: '19Qingfeng' }); }, 2000);
});
};
const getNormalData = () => {
return new Promise(r => {
setTimeout(() => { r({ name: 'wang.haoyu' }); }, 2000);
});
};
// Create router with loaders
const router = createBrowserRouter([
{
path: '/',
Component: App,
children: [
{
index: true,
Component: Normal,
loader: async () => {
const data = await getNormalData();
return json({ data });
}
},
{
path: 'deferred',
Component: Deferred,
loader: () => {
const deferredDataPromise = getDeferredData();
return defer({ deferredDataPromise });
}
}
]
}
]);The root path renders Normal with a blocking loader, while /deferred uses defer to return a promise without awaiting it.
RouterProvider
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { RouterProvider } from 'react-router-dom';
import router from './routes/router.tsx';
ReactDOM.createRoot(document.getElementById('root')!).render(
);The router object is passed to <RouterProvider> , which subscribes to router state changes and provides the data to child components.
useLoaderData / Suspense / Await
Components retrieve loader data via the useLoaderData hook. For deferred data, wrap the UI in Suspense and Await to show a fallback until the promise resolves.
import { useLoaderData } from 'react-router';
function Normal() {
const { data } = useLoaderData();
return (
Hello
{data.name}
);
}
export default Normal; import { Suspense } from 'react';
import { Await, useLoaderData } from 'react-router';
function Deferred() {
const { deferredDataPromise } = useLoaderData();
return (
<>
This is deferred Page!
Hello
{(data) =>
{data.name}
}
);
}
export default Deferred;When the route is visited, the router starts the loader, returns the promise, and Suspense displays the fallback until the promise settles.
Implementation Details
Loader Execution
During navigation, startNavigation matches the current route, calls handleLoaders (which awaits each loader), and stores the results as loaderData . After all loaders finish, completeNavigation updates the router state, which RouterProvider propagates via context.
Defer Mechanics
The defer function returns a DeferredData instance. Each value is wrapped with trackPromise , marking the promise as tracked ( _tracked = true ) and attaching _data or _error once settled.
Await Component
Await reads the tracked promise. If it is pending, it throws the promise so that the surrounding Suspense shows the fallback. When settled, it renders either the children (on success) or an error element (on failure).
Server‑Side Rendering
In SSR, loaders run only on the server; the client receives the rendered HTML and the serialized loader data. React Router’s hydrate process uses __staticRouterHydrationData to re‑hydrate the state on the client.
Remix extends this model by serializing pending promises and re‑using the same Await / Suspense logic on the client, allowing streaming of partially resolved data.
Conclusion
The article demonstrates the advantages, usage, and internal workings of React Router 6.4 Data APIs, showing how they improve both client‑side and server‑side rendering performance and provide a smoother, progressive loading experience.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.