Сессии и cookie
HTTP не помнит предыдущих запросов: каждый приходит на сервер сам по себе. Чтобы помнить пользователя между страницами, используют сессию — набор данных, привязанный к посетителю. Проще всего хранить сессию прямо в cookie: React Router подписывает её секретом, поэтому подделать содержимое нельзя.
Хранилище сессий
Создайте файл 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",
},
});Функция возвращает три помощника: getSession читает сессию из заголовка Cookie, commitSession превращает её в заголовок Set-Cookie, destroySession удаляет. Тип SessionData описывает постоянные данные, SessionFlashData — одноразовые.
Флеш-сообщение
Флеш-сообщение живёт до первого чтения. Удобно показать «Статья опубликована» после редиректа. Запишите его в действии app/routes/new-post.tsx:
// app/routes/new-post.tsx
import { commitSession, getSession } from "../sessions.server";
// в конце action, вместо прежнего return:
const session = await getSession(request.headers.get("Cookie"));
session.flash("message", "Статья опубликована");
return redirect(`/posts/${post.id}`, {
headers: { "Set-Cookie": await commitSession(session) },
});Сессия изменена на сервере, а заголовок Set-Cookie отправляет её браузеру.
Чтение в макете
Загрузчик макета app/routes/layout.tsx достаёт сообщение и снова сохраняет сессию — уже без него:
// 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 />
</>
);
}Опубликуйте статью — над ней появится «Статья опубликована». Обновите страницу — сообщение исчезнет.
Методы сессии
| Метод | Что делает |
|---|---|
session.get(ключ) | Читает значение |
session.set(ключ, значение) | Записывает |
session.flash(ключ, значение) | Записывает до первого чтения |
session.unset(ключ) | Удаляет одно значение |
session.has(ключ) | Проверяет наличие |
Частая ошибка
Изменить сессию и забыть вернуть заголовок Set-Cookie. Объект session существует только на время запроса: без commitSession в ответе браузер не узнает об изменениях, и при следующем запросе всё будет как раньше. Любой set, flash или unset должен заканчиваться ответом с этим заголовком.