Frontend Development 24 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Understanding React Router 6.4 Data APIs: Loader, Defer, and Await

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.

frontendSSRReact RouterawaitdeferData APIs
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.