# How Loaders Are Implemented in React Router for Declarative Data Fetching

> Discover how React Router implements loaders for declarative data fetching. Learn to use loader functions and the useLoaderData hook to streamline data handling in your React applications.

- Repository: [Remix/react-router](https://github.com/remix-run/react-router)
- Tags: internals
- Published: 2026-03-06

---

**React Router implements declarative data fetching by attaching a `loader` function to route definitions, which executes during navigation to provide data via the `useLoaderData` hook.**

The `remix-run/react-router` library transforms traditional imperative data fetching into a seamless, type-safe pipeline that runs on both server and client. By declaring loaders directly on route objects, the framework automatically handles data serialization, transport, and hydration without manual state management.

## Declaring Loaders in Route Definitions

Loaders attach declaratively to route objects using the `loader` property. In [`router/utils.ts`](https://github.com/remix-run/react-router/blob/main/router/utils.ts), the `LoaderFunction` type defines the contract: a function receiving `LoaderFunctionArgs` (request, params, context) and returning serializable data or a `Response`.

```typescript
// router/utils.ts
export interface LoaderFunction {
  (args: LoaderFunctionArgs): Promise<Response> | Response | Promise<unknown> | unknown;
}

```

*Source: Lines 341-345 in [`packages/react-router/lib/router/utils.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts).*

A typical route declaration pairs a component with its data loader:

```tsx
import { createBrowserRouter, RouterProvider } from "react-router";
import { loader as invoiceLoader } from "./routes/invoices";

const router = createBrowserRouter([
  {
    path: "/invoices",
    loader: invoiceLoader,          // Declarative data fetching attachment
    element: <InvoicesPage />,
  },
]);

```

## Server-Side Loader Execution

When navigation triggers a data request, React Router builds a **static handler** that orchestrates loader invocation. The core wrapping logic lives in [`server-runtime/routes.ts`](https://github.com/remix-run/react-router/blob/main/server-runtime/routes.ts).

### The Static Handler Wrapper

The router conditionally wraps each route's loader to handle execution context and prerendering shortcuts:

```typescript
// packages/react-router/lib/server-runtime/routes.ts
loader: route.module.loader
  ? async (args: RRLoaderFunctionArgs) => {
      // Prerendered data shortcut (SSG) handling omitted
      let val = await callRouteHandler(route.module.loader!, args);
      return val;
    }
  : undefined,

```

This wrapper checks for prerendered static data before invoking the user-defined loader, and handles redirect responses appropriately.

*Source: Lines 86-136 in [`packages/react-router/lib/server-runtime/routes.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/server-runtime/routes.ts).*

### Normalizing Loader Calls with callRouteHandler

The `callRouteHandler` function in [`server-runtime/data.ts`](https://github.com/remix-run/react-router/blob/main/server-runtime/data.ts) sanitizes the execution environment before invoking the loader:

```typescript
// packages/react-router/lib/server-runtime/data.ts
export async function callRouteHandler(
  handler: LoaderFunction | ActionFunction,
  args: LoaderFunctionArgs | ActionFunctionArgs,
) {
  let result = await handler({
    request: stripRoutesParam(stripIndexParam(args.request)),
    params: args.params,
    context: args.context,
    unstable_pattern: args.unstable_pattern,
  });

  // Convert loader data redirects to proper Responses
  if (isDataWithResponseInit(result) && result.init && isRedirectStatusCode(result.init.status)) {
    throw new Response(null, result.init);
  }

  return result;
}

```

**`callRouteHandler`** strips internal search parameters (`_routes`, `index`) and forwards a clean request object. It also transforms loader-returned `data(..., {status: 302})` constructs into actual `Response` objects so the router recognizes them as redirects.

*Source: Lines 21-44 in [`packages/react-router/lib/server-runtime/data.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/server-runtime/data.ts).*

## Client-Side Data Access

After server execution, results aggregate into the router's **state** under `loaderData`. The server serializes this into the HTML payload (or JSON for XHR navigation), enabling client-side hydration.

### Consuming Loader Data with useLoaderData

The `useLoaderData` hook in [`hooks.tsx`](https://github.com/remix-run/react-router/blob/main/hooks.tsx) provides type-safe access to the nearest route match's loader data:

```typescript
// packages/react-router/lib/hooks.tsx
export function useLoaderData<T = any>(): SerializeFrom<T> {
  const route = useRouteMatch(); // Gets the nearest UI match
  invariant(route, "useLoaderData must be used within a route element");
  return route.loaderData as SerializeFrom<T>;
}

```

Components consume loader data by inferring types directly from the loader function:

```tsx
import { useLoaderData } from "react-router";

export async function loader({ params }) {
  const res = await fetch(`/api/invoice/${params.id}`);
  return res.json();
}

export function Invoice() {
  const invoice = useLoaderData<typeof loader>(); // Type-inferred data
  return (
    <section>
      <h1>Invoice #{invoice.id}</h1>
      <p>Total: ${invoice.amount}</p>
    </section>
  );
}

```

The generic argument `typeof loader` ensures TypeScript recognizes the exact shape returned by the loader, providing end-to-end type safety from server fetch to React component.

*Source: Lines 1546-1564 in [`packages/react-router/lib/hooks.tsx`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx).*

## Summary

- **Declarative route configuration** attaches `loader` functions to route objects, defining data dependencies alongside UI components.
- **[`server-runtime/routes.ts`](https://github.com/remix-run/react-router/blob/main/server-runtime/routes.ts)** builds static handlers that wrap raw loaders with prerender checks and redirect handling.
- **`callRouteHandler`** in [`server-runtime/data.ts`](https://github.com/remix-run/react-router/blob/main/server-runtime/data.ts) sanitizes requests and normalizes loader returns, including automatic redirect transformations.
- **Router state management** aggregates results into `loaderData`, serializing them for client hydration during SSR or SSG.
- **`useLoaderData`** provides type-safe, zero-boilerplate access to loader results within route components.

## Frequently Asked Questions

### How does React Router decide when to call a loader?

React Router invokes loaders during navigation to a route containing the `loader` property. On the server, this happens during the request/response cycle. On the client, it occurs during SPA transitions, with the router fetching data via XHR and merging results into the existing state.

### Can loader functions return non-JSON data?

Loaders must return serializable data or web standard `Response` objects. While you can return primitives, objects, or arrays, functions, class instances, or circular structures will cause serialization errors. Return a `Response` instance when you need custom headers or status codes.

### What is the difference between `useLoaderData` and `useRouteLoaderData`?

**`useLoaderData`** returns data for the current route match (the component rendering immediately), while **`useRouteLoaderData`** accepts a route ID string to access ancestor or sibling route data from anywhere in the component tree, enabling cross-route data sharing without prop drilling.

### How do loaders handle errors and redirects?

Loaders can throw `Response` objects to trigger error boundaries, or return/throw redirects using `redirect()` helpers. The `callRouteHandler` function automatically detects `data(...)` calls with 3xx status codes and converts them to thrown `Response` objects, causing the router to execute navigation changes rather than rendering the component.