Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/remix-run/react-router/llms.txt

Use this file to discover all available pages before exploring further.

useLoaderData

Returns the data from the closest route loader or clientLoader.
This hook only works in Data and Framework modes.

Signature

function useLoaderData<T = any>(): SerializeFrom<T>

Parameters

None.

Returns

data
SerializeFrom<T>
The data returned from the route’s loader or clientLoader function. The type is automatically serialized for server loaders.

Usage

Basic usage

import { useLoaderData } from "react-router";

export async function loader() {
  const invoices = await db.invoices.findAll();
  return { invoices };
}

export default function Invoices() {
  const { invoices } = useLoaderData();
  
  return (
    <ul>
      {invoices.map((invoice) => (
        <li key={invoice.id}>{invoice.name}</li>
      ))}
    </ul>
  );
}

With TypeScript (Framework mode)

In Framework mode, types are automatically generated:
import type { Route } from "./+types.invoices";

export async function loader() {
  const invoices = await db.invoices.findAll();
  return { invoices };
}

export default function Invoices() {
  // Type is inferred from loader return type
  const { invoices } = useLoaderData<typeof loader>();
  
  return (
    <ul>
      {invoices.map((invoice) => (
        <li key={invoice.id}>{invoice.name}</li>
      ))}
    </ul>
  );
}

With TypeScript (Data mode)

In Data mode, manually type the hook:
interface LoaderData {
  invoices: Invoice[];
}

const loader = async () => {
  const invoices = await db.invoices.findAll();
  return { invoices };
};

function Invoices() {
  const { invoices } = useLoaderData<LoaderData>();
  
  return (
    <ul>
      {invoices.map((invoice) => (
        <li key={invoice.id}>{invoice.name}</li>
      ))}
    </ul>
  );
}

Return Response objects

You can return Response objects from loaders:
export async function loader() {
  const invoices = await db.invoices.findAll();
  
  return Response.json(
    { invoices },
    {
      headers: {
        "Cache-Control": "max-age=300",
      },
    }
  );
}

export default function Invoices() {
  // Response is automatically unwrapped
  const { invoices } = useLoaderData();
  return <InvoiceList invoices={invoices} />;
}

Client loaders

Access data from clientLoader:
export async function clientLoader() {
  // Run only in the browser
  const cachedData = localStorage.getItem("invoices");
  if (cachedData) {
    return JSON.parse(cachedData);
  }
  
  const invoices = await fetch("/api/invoices").then(r => r.json());
  localStorage.setItem("invoices", JSON.stringify(invoices));
  return invoices;
}

export default function Invoices() {
  const invoices = useLoaderData();
  return <InvoiceList invoices={invoices} />;
}

With route params

export async function loader({ params }) {
  const invoice = await db.invoices.find(params.invoiceId);
  
  if (!invoice) {
    throw new Response("Not Found", { status: 404 });
  }
  
  return { invoice };
}

export default function Invoice() {
  const { invoice } = useLoaderData();
  
  return (
    <div>
      <h1>{invoice.name}</h1>
      <p>Amount: ${invoice.amount}</p>
    </div>
  );
}

With request data

export async function loader({ request }) {
  const url = new URL(request.url);
  const query = url.searchParams.get("q");
  
  const results = await searchInvoices(query);
  return { results, query };
}

export default function SearchResults() {
  const { results, query } = useLoaderData();
  
  return (
    <div>
      <h1>Results for: {query}</h1>
      <ResultsList results={results} />
    </div>
  );
}

Common Patterns

Loading state

Loaders are called before the component renders, so there’s no loading state in the component:
export default function Invoices() {
  // Data is always available when component renders
  const { invoices } = useLoaderData();
  
  // No need for:
  // if (!invoices) return <Loading />;
  
  return <InvoiceList invoices={invoices} />;
}
Use useNavigation for global loading UI.

Error handling

Throw errors in loaders to render the ErrorBoundary:
export async function loader({ params }) {
  const invoice = await db.invoices.find(params.invoiceId);
  
  if (!invoice) {
    // This will render the ErrorBoundary
    throw new Response("Invoice not found", { status: 404 });
  }
  
  return { invoice };
}

export function ErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error) && error.status === 404) {
    return <div>Invoice not found</div>;
  }
  
  return <div>Something went wrong</div>;
}

export default function Invoice() {
  // invoice is always defined here
  const { invoice } = useLoaderData();
  return <InvoiceDetail invoice={invoice} />;
}

Parallel data loading

Load multiple resources in parallel:
export async function loader({ params }) {
  const [invoice, customer, payments] = await Promise.all([
    db.invoices.find(params.invoiceId),
    db.customers.find(params.customerId),
    db.payments.findByInvoice(params.invoiceId),
  ]);
  
  return { invoice, customer, payments };
}

export default function InvoiceDetail() {
  const { invoice, customer, payments } = useLoaderData();
  
  return (
    <div>
      <h1>{invoice.name}</h1>
      <CustomerInfo customer={customer} />
      <PaymentHistory payments={payments} />
    </div>
  );
}

Deferred data

Defer slow data loading:
import { defer, Await } from "react-router";
import { Suspense } from "react";

export async function loader() {
  // Fast data - await immediately
  const critical = await getCriticalData();
  
  // Slow data - defer loading
  const slow = getSlowData();
  
  return defer({ critical, slow });
}

export default function Page() {
  const { critical, slow } = useLoaderData();
  
  return (
    <div>
      {/* Renders immediately */}
      <CriticalData data={critical} />
      
      {/* Shows fallback while loading */}
      <Suspense fallback={<Loading />}>
        <Await resolve={slow}>
          {(data) => <SlowData data={data} />}
        </Await>
      </Suspense>
    </div>
  );
}

Type Safety

SerializeFrom

The SerializeFrom type ensures data is JSON-serializable:
// Date objects are converted to strings
export async function loader() {
  return {
    date: new Date(),  // Becomes string
    data: { id: 1 },
  };
}

export default function Component() {
  const data = useLoaderData<typeof loader>();
  
  // TypeScript knows date is a string, not Date
  data.date;  // string
}

Build docs developers (and LLMs) love