Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

MethodWhat 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.

Thanks for reading!

Donate to the site — we work hard for you.