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.
Loaders and Actions
Loaders and actions are the primary way to load and mutate data in React Router. They run on the server in Framework mode and enable powerful patterns like optimistic UI, automatic revalidation, and error handling.
Loaders
Loaders fetch data before a route renders. They’re called before your component renders, ensuring data is ready immediately.
Basic Loader
// app/routes/products.$id.tsx
import { useLoaderData } from "react-router";
export async function loader({ params }) {
const product = await db.products.find(params.id);
return { product };
}
export default function Product() {
const { product } = useLoaderData<typeof loader>();
return <h1>{product.name}</h1>;
}
Loader Function Signature
From lib/router/utils.ts, loaders receive LoaderFunctionArgs:
export interface LoaderFunctionArgs {
request: Request; // Web Fetch API Request
params: Params; // Route params like { id: "123" }
context?: unknown; // Server context from getLoadContext()
unstable_pattern: string; // The route pattern that matched
}
export interface LoaderFunction {
(args: LoaderFunctionArgs): Promise<Response> | Response | Promise<any> | any;
}
Accessing Request Data
Loaders receive a standard Web Fetch API Request:
export async function loader({ request, params }) {
// URL and search params
const url = new URL(request.url);
const searchTerm = url.searchParams.get("q");
// Headers
const userAgent = request.headers.get("User-Agent");
// Cookies (via headers)
const cookie = request.headers.get("Cookie");
return { searchTerm, userAgent };
}
Returning Data
Loaders can return various response types:
// Plain objects (automatically serialized)
export async function loader() {
return { user: { name: "John" } };
}
// Response objects
export async function loader() {
return new Response(JSON.stringify({ data }), {
headers: { "Content-Type": "application/json" }
});
}
// Responses with custom headers
export async function loader() {
return Response.json({ data }, {
headers: {
"Cache-Control": "max-age=300",
"Set-Cookie": "token=abc123"
}
});
}
Error Handling in Loaders
export async function loader({ params }) {
const product = await db.products.find(params.id);
if (!product) {
throw new Response("Not Found", { status: 404 });
}
return { product };
}
Errors thrown from loaders are caught by the nearest ErrorBoundary.
Actions
Actions handle data mutations triggered by form submissions or imperative calls.
Basic Action
// app/routes/products.$id.tsx
import { Form, redirect } from "react-router";
export async function action({ request, params }) {
const formData = await request.formData();
const updates = {
name: formData.get("name"),
price: formData.get("price"),
};
await db.products.update(params.id, updates);
return redirect(`/products/${params.id}`);
}
export default function EditProduct() {
return (
<Form method="post">
<input name="name" />
<input name="price" />
<button type="submit">Save</button>
</Form>
);
}
Action Function Signature
export interface ActionFunctionArgs {
request: Request;
params: Params;
context?: unknown;
unstable_pattern: string;
}
export interface ActionFunction {
(args: ActionFunctionArgs): Promise<Response> | Response | Promise<any> | any;
}
export async function action({ request }) {
const formData = await request.formData();
// Get individual fields
const email = formData.get("email");
const password = formData.get("password");
// Get all values for a field (for checkboxes)
const tags = formData.getAll("tags");
// Convert to object
const data = Object.fromEntries(formData);
// Handle file uploads
const avatar = formData.get("avatar"); // File object
return { success: true };
}
Different HTTP Methods
export async function action({ request, params }) {
switch (request.method) {
case "POST":
return createProduct(await request.formData());
case "PUT":
case "PATCH":
return updateProduct(params.id, await request.formData());
case "DELETE":
return deleteProduct(params.id);
default:
throw new Response("Method Not Allowed", { status: 405 });
}
}
Returning Action Data
Action data is available via useActionData:
import { useActionData, Form } from "react-router";
export async function action({ request }) {
const formData = await request.formData();
const errors = await validate(formData);
if (errors) {
return { errors }; // Return validation errors
}
await saveData(formData);
return { success: true };
}
export default function ContactForm() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<input name="email" />
{actionData?.errors?.email && (
<span>{actionData.errors.email}</span>
)}
<button>Submit</button>
{actionData?.success && <p>Saved!</p>}
</Form>
);
}
Data Flow
The complete data flow for navigation and mutations:
Revalidation
After an action completes, React Router automatically revalidates loaders to keep UI in sync.
Automatic Revalidation
// When this action completes...
export async function action() {
await db.products.create({ name: "New Product" });
return redirect("/products");
}
// ...this loader is automatically called again
export async function loader() {
return { products: await db.products.findAll() };
}
Controlling Revalidation
You can prevent revalidation when it’s not needed:
export function shouldRevalidate({ currentParams, nextParams }) {
// Only revalidate if the id changed
return currentParams.id !== nextParams.id;
}
From lib/router/router.ts, the shouldRevalidate function receives:
export interface ShouldRevalidateFunctionArgs {
currentUrl: URL;
currentParams: Params;
nextUrl: URL;
nextParams: Params;
formMethod?: FormMethod;
formAction?: string;
formEncType?: FormEncType;
formData?: FormData;
actionResult?: DataResult;
defaultShouldRevalidate: boolean;
}
Manual Revalidation
import { useRevalidator } from "react-router";
function ProductList() {
const revalidator = useRevalidator();
return (
<div>
<button onClick={() => revalidator.revalidate()}>
Refresh
</button>
{revalidator.state === "loading" && <Spinner />}
</div>
);
}
Parallel Data Loading
React Router loads all matching route loaders in parallel:
// All three loaders run simultaneously
const routes = [
{
path: "/",
loader: rootLoader, // Loads user session
children: [
{
path: "dashboard",
loader: dashboardLoader, // Loads dashboard data
children: [
{
path: "analytics",
loader: analyticsLoader, // Loads analytics data
}
]
}
]
}
];
Context and Request Context
In Framework mode, you can provide context to loaders and actions:
// Server setup
import { createRequestHandler } from "@react-router/node";
export default createRequestHandler({
build: require("./build/server"),
getLoadContext(request) {
return {
db: await getDatabase(),
user: await getUser(request),
};
},
});
Using context in loaders:
export async function loader({ context }) {
const products = await context.db.products.findAll();
return { products, user: context.user };
}
Redirects
Redirecting from loaders and actions:
import { redirect } from "react-router";
export async function loader({ request, context }) {
if (!context.user) {
return redirect("/login");
}
return { data };
}
export async function action({ request }) {
const product = await createProduct(await request.formData());
return redirect(`/products/${product.id}`);
}
From lib/router/utils.ts, redirect creates a special Response:
export const redirect = (
url: string,
init: number | ResponseInit = 302
): Response => {
let responseInit = init;
if (typeof responseInit === "number") {
responseInit = { status: responseInit };
}
return new Response(null, {
...responseInit,
headers: {
...responseInit.headers,
Location: url,
},
});
};
Deferred Data
For streaming SSR, you can defer slow data:
import { defer } from "react-router";
export async function loader() {
const criticalData = await getCriticalData(); // Wait for this
const slowData = getSlowData(); // Don't wait, return promise
return defer({
critical: criticalData,
slow: slowData, // Promise
});
}
Consume deferred data with <Await>:
import { Await, useLoaderData } from "react-router";
import { Suspense } from "react";
export default function Page() {
const { critical, slow } = useLoaderData<typeof loader>();
return (
<div>
<h1>{critical.title}</h1>
<Suspense fallback={<Spinner />}>
<Await resolve={slow}>
{(data) => <Details data={data} />}
</Await>
</Suspense>
</div>
);
}
Type Safety
React Router provides full type inference:
export async function loader() {
return {
user: { name: "John", age: 30 },
products: [{ id: 1, name: "Widget" }]
};
}
export default function Component() {
// TypeScript infers the exact type!
const { user, products } = useLoaderData<typeof loader>();
// ^? { name: string; age: number }
// ^? { id: number; name: string }[]
}
Best Practices
- Keep loaders focused - Each loader should fetch data for its route
- Use actions for mutations - Keep side effects in actions, not loaders
- Return semantic HTTP responses - Use proper status codes (404, 500, etc.)
- Leverage parallel loading - Structure routes to load data in parallel
- Handle errors gracefully - Throw responses to trigger error boundaries
- Use redirect after mutations - Follow the Post/Redirect/Get pattern
- Validate in actions - Return validation errors instead of throwing
- Revalidate wisely - Use
shouldRevalidate to optimize unnecessary loads