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
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
}