INDIE UNPKG

19.1 kBMarkdownView Raw
1---
2title: Sessions and Cookies
3---
4
5# Sessions and Cookies
6
7[MODES: framework, data]
8
9## Sessions
10
11Sessions are an important part of websites that allow the server to identify requests coming from the same person, especially when it comes to server-side form validation or when JavaScript is not on the page. Sessions are a fundamental building block of many sites that let users "log in", including social, e-commerce, business, and educational websites.
12
13When using React Router as your framework, sessions are managed on a per-route basis (rather than something like express middleware) in your `loader` and `action` methods using a "session storage" object (that implements the [`SessionStorage`][session-storage] interface). Session storage understands how to parse and generate cookies, and how to store session data in a database or filesystem.
14
15### Using Sessions
16
17This is an example of a cookie session storage:
18
19```ts filename=app/sessions.server.ts
20import { createCookieSessionStorage } from "react-router";
21
22type SessionData = {
23 userId: string;
24};
25
26type SessionFlashData = {
27 error: string;
28};
29
30const { getSession, commitSession, destroySession } =
31 createCookieSessionStorage<SessionData, SessionFlashData>(
32 {
33 // a Cookie from `createCookie` or the CookieOptions to create one
34 cookie: {
35 name: "__session",
36
37 // all of these are optional
38 domain: "reactrouter.com",
39 // Expires can also be set (although maxAge overrides it when used in combination).
40 // Note that this method is NOT recommended as `new Date` creates only one date on each server deployment, not a dynamic date in the future!
41 //
42 // expires: new Date(Date.now() + 60_000),
43 httpOnly: true,
44 maxAge: 60,
45 path: "/",
46 sameSite: "lax",
47 secrets: ["s3cret1"],
48 secure: true,
49 },
50 },
51 );
52
53export { getSession, commitSession, destroySession };
54```
55
56We recommend setting up your session storage object in `app/sessions.server.ts` so all routes that need to access session data can import from the same spot.
57
58The input/output to a session storage object are HTTP cookies. `getSession()` retrieves the current session from the incoming request's `Cookie` header, and `commitSession()`/`destroySession()` provide the `Set-Cookie` header for the outgoing response.
59
60You'll use methods to get access to sessions in your `loader` and `action` functions.
61
62### Session API
63
64After retrieving a session with `getSession()`, the returned session object has a handful of methods and properties:
65
66```tsx
67export async function action({
68 request,
69}: Route.ActionArgs) {
70 const session = await getSession(
71 request.headers.get("Cookie"),
72 );
73
74 session.get("foo");
75 session.has("bar");
76 // etc.
77
78 return redirect("/", {
79 headers: {
80 "Set-Cookie": await commitSession(session),
81 },
82 });
83}
84```
85
86<docs-warning>Every time you modify session data, you must `commitSession()` or your changes will be lost. When using cookie session storage, you must also send the `Set-Cookie` header it returns in your response.</docs-warning>
87
88See the [Session API][session-api] for all methods and properties available on the session object.
89
90#### `session.has(key)`
91
92Returns `true` if the session has a value (or an unread flash value) for the given `key`. This does not consume flash values.
93
94```ts
95session.has("userId");
96```
97
98#### `session.set(key, value)`
99
100Sets a session value for use in subsequent requests:
101
102```ts
103session.set("userId", "1234");
104```
105
106#### `session.flash(key, value)`
107
108Sets a session value that is only valid until the next `session.get()` for that key. After that, it's gone. Most useful for "flash messages" and server-side form validation messages:
109
110```ts
111session.flash(
112 "globalMessage",
113 "Project successfully archived",
114);
115```
116
117A flash value is typically set in an `action` before a redirect, then read in the `loader` of the page being redirected to:
118
119```tsx filename=app/routes/projects.tsx
120export async function action({
121 request,
122}: Route.ActionArgs) {
123 const session = await getSession(
124 request.headers.get("Cookie"),
125 );
126 // ...archive the project
127 session.flash(
128 "globalMessage",
129 "Project successfully archived",
130 );
131 return redirect("/projects", {
132 headers: {
133 "Set-Cookie": await commitSession(session),
134 },
135 });
136}
137
138export async function loader({
139 request,
140}: Route.LoaderArgs) {
141 const session = await getSession(
142 request.headers.get("Cookie"),
143 );
144 // Reading a flash value removes it from the session
145 const message = session.get("globalMessage");
146
147 return data(
148 { message },
149 {
150 headers: {
151 // Commit the session so the flash value is removed
152 "Set-Cookie": await commitSession(session),
153 },
154 },
155 );
156}
157```
158
159Flash values are read with `session.get()` using the same key they were set with. Reading a flash value removes it from the session, so you must still `commitSession()` after reading it; otherwise the flash value will be read again on the next request.
160
161Avoid using the same key for both `session.set()` and `session.flash()`. `session.get()` returns the regular value first, so the flash value would never be read or removed.
162
163You can type flash values separately from regular session values with the second generic on your session storage, like the `SessionFlashData` type in [Using Sessions](#using-sessions).
164
165#### `session.get(key)`
166
167Accesses a session value from a previous request:
168
169```ts
170session.get("name");
171```
172
173#### `session.unset(key)`
174
175Removes a value from the session. This does not remove pending flash values; those are only removed when read with `session.get()`.
176
177```ts
178session.unset("name");
179```
180
181### Login form example
182
183A login form might look something like this:
184
185```tsx filename=app/routes/login.tsx lines=[4-7,12-14,16,22-23,26,35-37,48-49,54,59,64]
186import { data, redirect } from "react-router";
187import type { Route } from "./+types/login";
188
189import {
190 getSession,
191 commitSession,
192} from "../sessions.server";
193
194export async function loader({
195 request,
196}: Route.LoaderArgs) {
197 const session = await getSession(
198 request.headers.get("Cookie"),
199 );
200
201 if (session.has("userId")) {
202 // Redirect to the home page if they are already signed in.
203 return redirect("/");
204 }
205
206 return data(
207 // Read and unset the flash message set by the route action.
208 { error: session.get("error") },
209 {
210 headers: {
211 "Set-Cookie": await commitSession(session),
212 },
213 },
214 );
215}
216
217export async function action({
218 request,
219}: Route.ActionArgs) {
220 const session = await getSession(
221 request.headers.get("Cookie"),
222 );
223 const form = await request.formData();
224 const username = form.get("username");
225 const password = form.get("password");
226
227 const userId = await validateCredentials(
228 username,
229 password,
230 );
231
232 if (userId == null) {
233 // Set a single-use flash message to be read by the route loader.
234 session.flash("error", "Invalid username/password");
235
236 // Redirect back to the login page with errors.
237 return redirect("/login", {
238 headers: {
239 "Set-Cookie": await commitSession(session),
240 },
241 });
242 }
243
244 session.set("userId", userId);
245
246 // Login succeeded, send them to the home page.
247 return redirect("/", {
248 headers: {
249 "Set-Cookie": await commitSession(session),
250 },
251 });
252}
253
254export default function Login({
255 loaderData,
256}: Route.ComponentProps) {
257 const { error } = loaderData;
258
259 return (
260 <div>
261 {error ? <div className="error">{error}</div> : null}
262 <form method="POST">
263 <div>
264 <p>Please sign in</p>
265 </div>
266 <label>
267 Username: <input type="text" name="username" />
268 </label>
269 <label>
270 Password:{" "}
271 <input type="password" name="password" />
272 </label>
273 </form>
274 </div>
275 );
276}
277```
278
279And then a logout form might look something like this:
280
281```tsx filename=app/routes/logout.tsx
282import {
283 getSession,
284 destroySession,
285} from "../sessions.server";
286import type { Route } from "./+types/logout";
287
288export async function action({
289 request,
290}: Route.ActionArgs) {
291 const session = await getSession(
292 request.headers.get("Cookie"),
293 );
294 return redirect("/login", {
295 headers: {
296 "Set-Cookie": await destroySession(session),
297 },
298 });
299}
300
301export default function LogoutRoute() {
302 return (
303 <>
304 <p>Are you sure you want to log out?</p>
305 <Form method="post">
306 <button>Logout</button>
307 </Form>
308 <Link to="/">Never mind</Link>
309 </>
310 );
311}
312```
313
314<docs-warning>It's important that you logout (or perform any mutation for that matter) in an `action` and not a `loader`. Otherwise you open your users to [Cross-Site Request Forgery][csrf] attacks.</docs-warning>
315
316### Session Gotchas
317
318Because of nested routes, multiple loaders can be called to construct a single page. When using `session.flash()` or `session.unset()`, you need to be sure no other loaders in the request are going to want to read that, otherwise you'll get race conditions. Typically if you're using flash, you'll want to have a single loader read it, if another loader wants a flash message, use a different key for that loader.
319
320### Creating custom session storage
321
322React Router makes it easy to store sessions in your own database if needed. The [`createSessionStorage()`][create-session-storage] API requires a `cookie` (for options for creating a cookie, see [cookies][cookies]) and a set of create, read, update, and delete (CRUD) methods for managing the session data. The cookie is used to persist the session ID.
323
324- `createData` will be called from `commitSession` on the initial session creation when no session ID exists in the cookie
325- `readData` will be called from `getSession` when a session ID exists in the cookie
326- `updateData` will be called from `commitSession` when a session ID already exists in the cookie
327- `deleteData` is called from `destroySession`
328
329The following example shows how you could do this using a generic database client:
330
331```ts
332import { createSessionStorage } from "react-router";
333
334function createDatabaseSessionStorage({
335 cookie,
336 host,
337 port,
338}) {
339 // Configure your database client...
340 const db = createDatabaseClient(host, port);
341
342 return createSessionStorage({
343 cookie,
344 async createData(data, expires) {
345 // `expires` is a Date after which the data should be considered
346 // invalid. You could use it to invalidate the data somehow or
347 // automatically purge this record from your database.
348 const id = await db.insert(data);
349 return id;
350 },
351 async readData(id) {
352 return (await db.select(id)) || null;
353 },
354 async updateData(id, data, expires) {
355 await db.update(id, data);
356 },
357 async deleteData(id) {
358 await db.delete(id);
359 },
360 });
361}
362```
363
364And then you can use it like this:
365
366```ts
367const { getSession, commitSession, destroySession } =
368 createDatabaseSessionStorage({
369 host: "localhost",
370 port: 1234,
371 cookie: {
372 name: "__session",
373 sameSite: "lax",
374 },
375 });
376```
377
378The `expires` argument to `createData` and `updateData` is the same `Date` at which the cookie itself expires and is no longer valid. You can use this information to automatically purge the session record from your database to save on space, or to ensure that you do not otherwise return any data for old, expired cookies.
379
380### Additional session utils
381
382There are also several other session utilities available if you need them:
383
384- [`isSession`][is-session]
385- [`createMemorySessionStorage`][create-memory-session-storage] (local dev and testing)
386- [`createSession`][create-session] (custom storage)
387- [`createFileSessionStorage`][create-file-session-storage] (node)
388- [`createWorkersKVSessionStorage`][create-workers-kv-session-storage] (Cloudflare Workers)
389- [`createArcTableSessionStorage`][create-arc-table-session-storage] (architect, Amazon DynamoDB)
390
391## Cookies
392
393A [cookie][cookie] is a small piece of information that your server sends someone in a HTTP response that their browser will send back on subsequent requests. This technique is a fundamental building block of many interactive websites that adds state so you can build authentication (see [sessions][sessions]), shopping carts, user preferences, and many other features that require remembering who is "logged in".
394
395React Router's [`Cookie` interface][cookie-api] provides a logical, reusable container for cookie metadata.
396
397### Using cookies
398
399While you may create these cookies manually, it is more common to use a [session storage][sessions].
400
401In React Router, you will typically work with cookies in your `loader` and/or `action` functions, since those are the places where you need to read and write data.
402
403Let's say you have a banner on your e-commerce site that prompts users to check out the items you currently have on sale. The banner spans the top of your homepage, and includes a button on the side that allows the user to dismiss the banner so they don't see it for at least another week.
404
405First, create a cookie:
406
407```ts filename=app/cookies.server.ts
408import { createCookie } from "react-router";
409
410export const userPrefs = createCookie("user-prefs", {
411 maxAge: 604_800, // one week
412});
413```
414
415Then, you can `import` the cookie and use it in your `loader` and/or `action`. The `loader` in this case just checks the value of the user preference so you can use it in your component for deciding whether to render the banner. When the button is clicked, the `<form>` calls the `action` on the server and reloads the page without the banner.
416
417### User preferences example
418
419```tsx filename=app/routes/home.tsx lines=[4,9-11,18-20,29]
420import { Link, Form, redirect } from "react-router";
421import type { Route } from "./+types/home";
422
423import { userPrefs } from "../cookies.server";
424
425export async function loader({
426 request,
427}: Route.LoaderArgs) {
428 const cookieHeader = request.headers.get("Cookie");
429 const cookie =
430 (await userPrefs.parse(cookieHeader)) || {};
431 return { showBanner: cookie.showBanner };
432}
433
434export async function action({
435 request,
436}: Route.ActionArgs) {
437 const cookieHeader = request.headers.get("Cookie");
438 const cookie =
439 (await userPrefs.parse(cookieHeader)) || {};
440 const bodyParams = await request.formData();
441
442 if (bodyParams.get("bannerVisibility") === "hidden") {
443 cookie.showBanner = false;
444 }
445
446 return redirect("/", {
447 headers: {
448 "Set-Cookie": await userPrefs.serialize(cookie),
449 },
450 });
451}
452
453export default function Home({
454 loaderData,
455}: Route.ComponentProps) {
456 return (
457 <div>
458 {loaderData.showBanner ? (
459 <div>
460 <Link to="/sale">Don't miss our sale!</Link>
461 <Form method="post">
462 <input
463 type="hidden"
464 name="bannerVisibility"
465 value="hidden"
466 />
467 <button type="submit">Hide</button>
468 </Form>
469 </div>
470 ) : null}
471 <h1>Welcome!</h1>
472 </div>
473 );
474}
475```
476
477### Cookie attributes
478
479Cookies have [several attributes][cookie-attrs] that control when they expire, how they are accessed, and where they are sent. Any of these attributes may be specified either in `createCookie(name, options)`, or during `serialize()` when the `Set-Cookie` header is generated.
480
481```ts
482const cookie = createCookie("user-prefs", {
483 // These are defaults for this cookie.
484 path: "/",
485 sameSite: "lax",
486 httpOnly: true,
487 secure: true,
488 expires: new Date(Date.now() + 60_000),
489 maxAge: 60,
490});
491
492// You can either use the defaults:
493cookie.serialize(userPrefs);
494
495// Or override individual ones as needed:
496cookie.serialize(userPrefs, { sameSite: "strict" });
497```
498
499Please read [more info about these attributes][cookie-attrs] to get a better understanding of what they do.
500
501### Signing cookies
502
503It is possible to sign a cookie to automatically verify its contents when it is received. Since it's relatively easy to spoof HTTP headers, this is a good idea for any information that you do not want someone to be able to fake, like authentication information (see [sessions][sessions]).
504
505To sign a cookie, provide one or more `secrets` when you first create the cookie:
506
507```ts
508const cookie = createCookie("user-prefs", {
509 secrets: ["s3cret1"],
510});
511```
512
513Cookies that have one or more `secrets` will be stored and verified in a way that ensures the cookie's integrity.
514
515Secrets may be rotated by adding new secrets to the front of the `secrets` array. Cookies that have been signed with old secrets will still be decoded successfully in `cookie.parse()`, and the newest secret (the first one in the array) will always be used to sign outgoing cookies created in `cookie.serialize()`.
516
517```ts filename=app/cookies.server.ts
518export const cookie = createCookie("user-prefs", {
519 secrets: ["n3wsecr3t", "olds3cret"],
520});
521```
522
523```tsx filename=app/routes/my-route.tsx
524import { data } from "react-router";
525import { cookie } from "../cookies.server";
526import type { Route } from "./+types/my-route";
527
528export async function loader({
529 request,
530}: Route.LoaderArgs) {
531 const oldCookie = request.headers.get("Cookie");
532 // oldCookie may have been signed with "olds3cret", but still parses ok
533 const value = await cookie.parse(oldCookie);
534
535 return data("...", {
536 headers: {
537 // Set-Cookie is signed with "n3wsecr3t"
538 "Set-Cookie": await cookie.serialize(value),
539 },
540 });
541}
542```
543
544### Additional cookie utils
545
546There are also several other cookie utilities available if you need them:
547
548- [`isCookie`][is-cookie]
549- [`createCookie`][create-cookie]
550
551To learn more about each attribute, please see the [MDN Set-Cookie docs][cookie-attrs].
552
553[csrf]: https://developer.mozilla.org/en-US/docs/Glossary/CSRF
554[cookies]: #cookies
555[sessions]: #sessions
556[session-storage]: https://api.reactrouter.com/v8/interfaces/react-router.SessionStorage
557[session-api]: https://api.reactrouter.com/v8/interfaces/react-router.Session
558[is-session]: https://api.reactrouter.com/v8/variables/react-router.isSession.html
559[cookie-api]: https://api.reactrouter.com/v8/interfaces/react-router.Cookie
560[create-session-storage]: https://api.reactrouter.com/v8/functions/react-router.createSessionStorage
561[create-session]: https://api.reactrouter.com/v8/variables/react-router.createSession.html
562[create-memory-session-storage]: https://api.reactrouter.com/v8/functions/react-router.createMemorySessionStorage
563[create-file-session-storage]: https://api.reactrouter.com/v8/functions/_react-router_node.createFileSessionStorage
564[create-workers-kv-session-storage]: https://api.reactrouter.com/v8/functions/_react-router_cloudflare.createWorkersKVSessionStorage
565[create-arc-table-session-storage]: https://api.reactrouter.com/v8/functions/_react-router_architect.createArcTableSessionStorage
566[cookie]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
567[cookie-attrs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes
568[is-cookie]: https://api.reactrouter.com/v8/variables/react-router.isCookie.html
569[create-cookie]: https://api.reactrouter.com/v8/functions/react-router.createCookie