Sessions and cookies
HTTP does not remember previous requests: each one reaches the server on its own. To remember a user across pages you use a session — a set of data tied to a visitor. The simplest option is to store the session right in a cookie: React Router signs it with a secret, so its contents cannot be forged.
The session storage
Create app/sessions.server.ts:
// app/sessions.server.ts
import { createCookieSessionStorage } from "react-router";
type SessionData = { userId: string };
type SessionFlashData = { message: string };
export const { getSession, commitSession, destroySession } =
createCookieSessionStorage<SessionData, SessionFlashData>({
cookie: {
name: "__session",
httpOnly: true,
path: "/",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7,
secrets: [process.env.SESSION_SECRET ?? "dev-secret"],
secure: process.env.NODE_ENV === "production",
},
});The function returns three helpers: getSession reads the session from the Cookie header, commitSession turns it into a Set-Cookie header, destroySession removes it. SessionData describes persistent data, SessionFlashData one-time data.
A flash message
A flash message lives until it is first read. It is handy for showing "Post published" after a redirect. Write it in the action in app/routes/new-post.tsx:
// app/routes/new-post.tsx
import { commitSession, getSession } from "../sessions.server";
// at the end of the action, instead of the old return:
const session = await getSession(request.headers.get("Cookie"));
session.flash("message", "Post published");
return redirect(`/posts/${post.id}`, {
headers: { "Set-Cookie": await commitSession(session) },
});The session is changed on the server, and the Set-Cookie header sends it to the browser.
Reading it in the layout
The layout loader in app/routes/layout.tsx takes the message out and saves the session again — now without it:
// app/routes/layout.tsx
import { Outlet, data } from "react-router";
import type { Route } from "./+types/layout";
import { commitSession, getSession } from "../sessions.server";
export async function loader({ request }: Route.LoaderArgs) {
const session = await getSession(request.headers.get("Cookie"));
const message = session.get("message") ?? null;
return data(
{ message },
{ headers: { "Set-Cookie": await commitSession(session) } },
);
}
export default function Layout({ loaderData }: Route.ComponentProps) {
return (
<>
{loaderData.message && <p className="flash">{loaderData.message}</p>}
<Outlet />
</>
);
}Publish a post — "Post published" appears above it. Reload the page — the message is gone.
Session methods
| Method | What it does |
|---|---|
session.get(key) | Reads a value |
session.set(key, value) | Writes a value |
session.flash(key, value) | Writes until the first read |
session.unset(key) | Removes one value |
session.has(key) | Checks whether it exists |
Common mistake
Changing the session and forgetting to return the Set-Cookie header. The session object exists only for the duration of the request: without commitSession in the response the browser never learns about the changes, and the next request sees everything as before. Every set, flash or unset must end with a response carrying that header.