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.
Headers and Cookies
Learn how to work with HTTP headers and cookies in React Router applications.
Overview
React Router provides APIs for managing HTTP headers through the headers export and cookies through the Cookie API. These work seamlessly with loaders, actions, and middleware.
Access request headers in loaders and actions:
// app/routes/api.data.tsx
import type { Route } from "./+types/api.data";
export async function loader({ request }: Route.LoaderArgs) {
const userAgent = request.headers.get("User-Agent");
const acceptLanguage = request.headers.get("Accept-Language");
const referer = request.headers.get("Referer");
return {
userAgent,
language: acceptLanguage,
referer,
};
}
Return custom headers from loaders and actions:
import { json } from "react-router";
import type { Route } from "./+types/api.data";
export async function loader({ request }: Route.LoaderArgs) {
const data = await fetchData();
return json(data, {
headers: {
"Cache-Control": "public, max-age=3600",
"X-Custom-Header": "value",
},
});
}
Use the headers export for advanced header management:
// app/routes/blog.$slug.tsx
import type { Route } from "./+types/blog.$slug";
import type { HeadersArgs } from "react-router";
export async function loader({ params }: Route.LoaderArgs) {
const post = await getPost(params.slug);
return json(post, {
headers: {
"Cache-Control": "public, max-age=3600",
},
});
}
export function headers({ loaderHeaders, parentHeaders }: HeadersArgs) {
return {
// Use loader's cache header
"Cache-Control": loaderHeaders.get("Cache-Control") || "no-cache",
// Inherit parent headers
"X-Custom": parentHeaders.get("X-Custom") || "default",
};
}
Working with Cookies
Create and manage cookies using the Cookie API:
// app/cookies.ts
import { createCookie } from "react-router";
export const userPrefs = createCookie("user-prefs", {
maxAge: 60 * 60 * 24 * 365, // 1 year
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
});
Reading Cookies
Parse cookies in loaders and actions:
import { userPrefs } from "~/cookies";
import type { Route } from "./+types/preferences";
export async function loader({ request }: Route.LoaderArgs) {
const cookieHeader = request.headers.get("Cookie");
const prefs = await userPrefs.parse(cookieHeader);
return {
theme: prefs?.theme || "light",
language: prefs?.language || "en",
};
}
Setting Cookies
Set cookies in action responses:
import { redirect } from "react-router";
import { userPrefs } from "~/cookies";
import type { Route } from "./+types/preferences";
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const theme = formData.get("theme");
const cookie = await userPrefs.serialize({ theme });
return redirect("/", {
headers: {
"Set-Cookie": cookie,
},
});
}
Signed Cookies
Sign cookies to prevent tampering:
import { createCookie } from "react-router";
export const sessionCookie = createCookie("session", {
secrets: [process.env.SESSION_SECRET],
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 60 * 60 * 24 * 7, // 1 week
});
Multiple Cookies
Set multiple cookies in one response:
import { redirect } from "react-router";
import { sessionCookie, userPrefs } from "~/cookies";
import type { Route } from "./+types/login";
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const user = await login(formData);
const session = await sessionCookie.serialize({ userId: user.id });
const prefs = await userPrefs.serialize({ theme: user.theme });
return redirect("/dashboard", {
headers: [
["Set-Cookie", session],
["Set-Cookie", prefs],
],
});
}
Deleting Cookies
Expire cookies to delete them:
import { redirect } from "react-router";
import { sessionCookie } from "~/cookies";
import type { Route } from "./+types/logout";
export async function action({ request }: Route.ActionArgs) {
const cookie = await sessionCookie.serialize("", {
maxAge: 0,
});
return redirect("/", {
headers: {
"Set-Cookie": cookie,
},
});
}
Cookie Options
Available cookie options:
import { createCookie } from "react-router";
const cookie = createCookie("name", {
// Security
httpOnly: true, // Prevents JavaScript access
secure: true, // HTTPS only
sameSite: "strict", // CSRF protection: "strict" | "lax" | "none"
// Lifetime
maxAge: 3600, // Seconds until expiration
expires: new Date("2025-01-01"), // Absolute expiration date
// Scope
domain: ".example.com", // Cookie domain
path: "/", // Cookie path
// Signing
secrets: ["secret1", "secret2"], // Secret keys for signing
});
Manage caching with appropriate headers:
import { json } from "react-router";
import type { Route } from "./+types/api.data";
export async function loader({}: Route.LoaderArgs) {
const data = await fetchData();
// Cache for 1 hour
return json(data, {
headers: {
"Cache-Control": "public, max-age=3600",
},
});
}
// Never cache
export async function action({}: Route.ActionArgs) {
return json(
{ success: true },
{
headers: {
"Cache-Control": "no-store, no-cache, must-revalidate",
},
}
);
}
// Cache but revalidate
export async function loader2({}: Route.LoaderArgs) {
return json(data, {
headers: {
"Cache-Control": "public, max-age=0, must-revalidate",
ETag: generateETag(data),
},
});
}
Handle cross-origin requests:
import { json } from "react-router";
import type { Route } from "./+types/api.public";
export async function loader({ request }: Route.LoaderArgs) {
const data = await fetchPublicData();
return json(data, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
export async function action({ request }: Route.ActionArgs) {
// Handle preflight request
if (request.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
// Handle actual request
const result = await processRequest(request);
return json(result, {
headers: {
"Access-Control-Allow-Origin": "*",
},
});
}
Merge headers from multiple sources:
import type { HeadersArgs } from "react-router";
export function headers({
loaderHeaders,
parentHeaders,
actionHeaders,
errorHeaders,
}: HeadersArgs) {
const headers = new Headers();
// Prioritize action headers
if (actionHeaders.has("Set-Cookie")) {
actionHeaders.getSetCookie().forEach((cookie) => {
headers.append("Set-Cookie", cookie);
});
}
// Add loader cache control
const cacheControl = loaderHeaders.get("Cache-Control");
if (cacheControl) {
headers.set("Cache-Control", cacheControl);
}
// Inherit custom headers from parent
const customHeader = parentHeaders.get("X-Custom");
if (customHeader) {
headers.set("X-Custom", customHeader);
}
return headers;
}
Best Practices
- Use httpOnly for sensitive cookies - Prevents XSS attacks
- Always use secure in production - Ensures HTTPS-only transmission
- Set appropriate SameSite - Protects against CSRF attacks
- Sign sensitive cookies - Prevents tampering
- Set reasonable expiration - Balance convenience and security
- Use Cache-Control wisely - Improve performance without serving stale data
- Handle CORS properly - Allow legitimate cross-origin requests while maintaining security
- Rotate cookie secrets - Keep multiple secrets for zero-downtime rotation