Skip to content

Commit 2f26deb

Browse files
docs(utils/sessions): improve docs (#7247)
Co-authored-by: Michaël De Boey <[email protected]>
1 parent bfe1b17 commit 2f26deb

File tree

2 files changed

+44
-122
lines changed

2 files changed

+44
-122
lines changed

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
- aaronadamsCA
12
- aaronpowell96
23
- aaronshaf
34
- AbePlays

docs/utils/sessions.md

Lines changed: 43 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,16 @@ Remix comes with several pre-built session storage options for common scenarios,
1212

1313
- `createCookieSessionStorage`
1414
- `createMemorySessionStorage`
15-
- `createFileSessionStorage` (node)
16-
- `createWorkersKVSessionStorage` (Cloudflare Workers)
17-
- `createArcTableSessionStorage` (architect, Amazon DynamoDB)
15+
- `createFileSessionStorage` (Node.js, Deno)
16+
- `createWorkersKVSessionStorage` (Cloudflare)
17+
- `createArcTableSessionStorage` (Architect, Amazon DynamoDB)
1818
- custom storage with `createSessionStorage`
1919

2020
## Using Sessions
2121

2222
This is an example of a cookie session storage:
2323

24-
```ts filename=app/sessions.ts
25-
// app/sessions.ts
24+
```ts filename=app/sessions.server.ts
2625
import { createCookieSessionStorage } from "@remix-run/node"; // or cloudflare/deno
2726

2827
type SessionData = {
@@ -59,23 +58,23 @@ const { getSession, commitSession, destroySession } =
5958
export { getSession, commitSession, destroySession };
6059
```
6160

62-
We recommend setting up your session storage object in `app/sessions.ts` so all routes that need to access session data can import from the same spot (also, see our [Route Module Constraints][constraints]).
61+
We 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 (also, see our [Route Module Constraints][constraints]).
6362

6463
The 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.
6564

6665
You'll use methods to get access to sessions in your `loader` and `action` functions.
6766

6867
A login form might look something like this:
6968

70-
```tsx filename=app/routes/login.tsx lines=[8,13-15,17,22,26,34-36,47,52,57,62]
69+
```tsx filename=app/routes/login.js lines=[8,13-15,17,23-24,29-30,38-40,51-52,57,62,67]
7170
import type {
7271
ActionFunctionArgs,
7372
LoaderFunctionArgs,
7473
} from "@remix-run/node"; // or cloudflare/deno
7574
import { json, redirect } from "@remix-run/node"; // or cloudflare/deno
7675
import { useLoaderData } from "@remix-run/react";
7776

78-
import { getSession, commitSession } from "../sessions";
77+
import { getSession, commitSession } from "../sessions.server";
7978

8079
export async function loader({
8180
request,
@@ -89,10 +88,14 @@ export async function loader({
8988
return redirect("/");
9089
}
9190

92-
const data = { error: session.get("error") };
91+
const data = {
92+
// Read and unset the flash message set by the route action.
93+
error: session.get("error"),
94+
};
9395

9496
return json(data, {
9597
headers: {
98+
// Commit the updated session data.
9699
"Set-Cookie": await commitSession(session),
97100
},
98101
});
@@ -114,6 +117,7 @@ export async function action({
114117
);
115118

116119
if (userId == null) {
120+
// Set a single-use flash message to be read by the route loader.
117121
session.flash("error", "Invalid username/password");
118122

119123
// Redirect back to the login page with errors.
@@ -160,7 +164,7 @@ export default function Login() {
160164
And then a logout form might look something like this:
161165

162166
```tsx
163-
import { getSession, destroySession } from "../sessions";
167+
import { getSession, destroySession } from "../sessions.server";
164168

165169
export const action = async ({
166170
request,
@@ -192,7 +196,9 @@ export default function LogoutRoute() {
192196

193197
## Session Gotchas
194198

195-
Because 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.
199+
- Because 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.
200+
201+
- Every time you modify session data, you must `commitSession()` or your changes will be lost. This is different than what you might be used to, where some type of middleware automatically commits session data for you.
196202

197203
## `createSession`
198204

@@ -275,9 +281,11 @@ The `expires` argument to `createData` and `updateData` is the same `Date` at wh
275281

276282
For purely cookie-based sessions (where the session data itself is stored in the session cookie with the browser, see [cookies][cookies]) you can use `createCookieSessionStorage()`.
277283

278-
The main advantage of cookie session storage is that you don't need any additional backend services or databases to use it. It can also be beneficial in some load-balanced scenarios. However, cookie-based sessions may not exceed the browser's max-allowed cookie length (typically 4kb).
284+
The main advantage of cookie session storage is that you don't need any additional backend services or databases to use it. It can also be beneficial in some load-balanced scenarios.
285+
286+
However, cookie-based sessions may not exceed browser cookie size limits of 4k bytes. If your cookie size exceeds this limit, `commitSession()` will throw an error.
279287

280-
The downside is that you have to `commitSession` in almost every loader and action. If your loader or action changes the session at all, it must be committed. That means if you `session.flash` in an action, and then `session.get` in another, you must commit it for that flashed message to go away. With other session storage strategies you only have to commit it when it's created (the browser cookie doesn't need to change because it doesn't store the session data, just the key to find it elsewhere).
288+
The other downside is that you need to update the `Set-Cookie` header in every loader and action that modifies the session (this includes reading a flashed session value). With other strategies you only need to set the session cookie once, because it doesn't actually store any session data, just the key to find it elsewhere.
281289

282290
```ts
283291
import { createCookieSessionStorage } from "@remix-run/node"; // or cloudflare/deno
@@ -301,7 +309,7 @@ This storage keeps all the cookie information in your server's memory.
301309

302310
<docs-error>This should only be used in development. Use one of the other methods in production.</docs-error>
303311

304-
```ts filename=app/sessions.ts
312+
```ts filename=app/sessions.server.ts
305313
import {
306314
createCookie,
307315
createMemorySessionStorage,
@@ -321,15 +329,15 @@ const { getSession, commitSession, destroySession } =
321329
export { getSession, commitSession, destroySession };
322330
```
323331

324-
## `createFileSessionStorage` (node)
332+
## `createFileSessionStorage` (Node.js, Deno)
325333

326334
For file-backed sessions, use `createFileSessionStorage()`. File session storage requires a file system, but this should be readily available on most cloud providers that run express, maybe with some extra configuration.
327335

328-
The advantage of file-backed sessions is that only the session ID is stored in the cookie while the rest of the data is stored in a regular file on disk, ideal for sessions with more than 4kb of data.
336+
The advantage of file-backed sessions is that only the session ID is stored in the cookie while the rest of the data is stored in a regular file on disk, ideal for sessions with more than 4k bytes of data.
329337

330338
<docs-info>If you are deploying to a serverless function, ensure you have access to a persistent file system. They usually don't have one without extra configuration.</docs-info>
331339

332-
```ts filename=app/sessions.ts
340+
```ts filename=app/sessions.server.ts
333341
import {
334342
createCookie,
335343
createFileSessionStorage,
@@ -352,11 +360,11 @@ const { getSession, commitSession, destroySession } =
352360
export { getSession, commitSession, destroySession };
353361
```
354362

355-
## `createWorkersKVSessionStorage` (Cloudflare Workers)
363+
## `createWorkersKVSessionStorage` (Cloudflare)
356364

357365
For [Cloudflare Workers KV][cloudflare-kv] backed sessions, use `createWorkersKVSessionStorage()`.
358366

359-
The advantage of KV backed sessions is that only the session ID is stored in the cookie while the rest of the data is stored in a globally-replicated, low-latency data store with exceptionally high-read volumes with low-latency.
367+
The advantage of KV-backed sessions is that only the session ID is stored in the cookie while the rest of the data is stored in a globally replicated, low-latency data store supporting exceptionally high read volumes.
360368

361369
```ts filename=app/sessions.server.ts
362370
import {
@@ -380,11 +388,11 @@ const { getSession, commitSession, destroySession } =
380388
export { getSession, commitSession, destroySession };
381389
```
382390

383-
## `createArcTableSessionStorage` (architect, Amazon DynamoDB)
391+
## `createArcTableSessionStorage` (Architect, Amazon DynamoDB)
384392

385393
For [Amazon DynamoDB][amazon-dynamo-db] backed sessions, use `createArcTableSessionStorage()`.
386394

387-
The advantage of DynamoDB backed sessions is that only the session ID is stored in the cookie while the rest of the data is stored in a globally replicated, low-latency data store with exceptionally high read volumes with low-latency.
395+
The advantage of DynamoDB-backed sessions is that only the session ID is stored in the cookie while the rest of the data is stored in a globally replicated, low-latency data store supporting exceptionally high read volumes.
388396

389397
```
390398
# app.arc
@@ -422,7 +430,7 @@ export { getSession, commitSession, destroySession };
422430

423431
## Session API
424432

425-
After retrieving a session with `getSession`, the returned session object has a handful of methods and properties:
433+
After retrieving a session with `getSession()`, the returned session object has a handful of methods to read and update the retrieved session data:
426434

427435
```tsx
428436
export async function action({
@@ -431,12 +439,19 @@ export async function action({
431439
const session = await getSession(
432440
request.headers.get("Cookie")
433441
);
442+
434443
session.get("foo");
435-
session.has("bar");
444+
session.unset("bar");
436445
// etc.
446+
447+
await commitSession(session);
437448
}
438449
```
439450

451+
<docs-warning>Every time you modify session data, you must `commitSession()` or your changes will be lost.</docs-warning>
452+
453+
<docs-warning>When using cookie session storage, you must `Set-Cookie` every time you `commitSession()` or your changes will be lost.</docs-warning>
454+
440455
### `session.has(key)`
441456

442457
Returns `true` if the session has a variable with the given `name`.
@@ -455,122 +470,28 @@ session.set("userId", "1234");
455470

456471
### `session.flash(key, value)`
457472

458-
Sets a session value that will be unset the first time it is read. After that, it's gone. Most useful for "flash messages" and server-side form validation messages:
459-
460-
```tsx
461-
import { commitSession, getSession } from "../sessions";
462-
463-
export async function action({
464-
params,
465-
request,
466-
}: ActionFunctionArgs) {
467-
const session = await getSession(
468-
request.headers.get("Cookie")
469-
);
470-
const deletedProject = await archiveProject(
471-
params.projectId
472-
);
473-
474-
session.flash(
475-
"globalMessage",
476-
`Project ${deletedProject.name} successfully archived`
477-
);
478-
479-
return redirect("/dashboard", {
480-
headers: {
481-
"Set-Cookie": await commitSession(session),
482-
},
483-
});
484-
}
485-
```
486-
487-
Now we can read the message in a loader.
488-
489-
<docs-info>You must commit the session whenever you read a `flash`. This is different than what you might be used to, where some type of middleware automatically sets the cookie header for you.</docs-info>
490-
491-
```tsx
492-
import { json } from "@remix-run/node"; // or cloudflare/deno
493-
import {
494-
Meta,
495-
Links,
496-
Scripts,
497-
Outlet,
498-
} from "@remix-run/react";
499-
500-
import { getSession, commitSession } from "./sessions";
473+
Sets a session value that will be unset the first time it is read in a subsequent request. After that, it's gone. Most useful for "flash messages" and server-side form validation messages:
501474

502-
export async function loader({
503-
request,
504-
}: LoaderFunctionArgs) {
505-
const session = await getSession(
506-
request.headers.get("Cookie")
507-
);
508-
const message = session.get("globalMessage") || null;
509-
510-
return json(
511-
{ message },
512-
{
513-
headers: {
514-
// only necessary with cookieSessionStorage
515-
"Set-Cookie": await commitSession(session),
516-
},
517-
}
518-
);
519-
}
520-
521-
export default function App() {
522-
const { message } = useLoaderData<typeof loader>();
523-
524-
return (
525-
<html>
526-
<head>
527-
<Meta />
528-
<Links />
529-
</head>
530-
<body>
531-
{message ? (
532-
<div className="flash">{message}</div>
533-
) : null}
534-
<Outlet />
535-
<Scripts />
536-
</body>
537-
</html>
538-
);
539-
}
475+
```ts
476+
session.flash("globalMessage", "Project successfully archived");
540477
```
541478

542-
### `session.get()`
479+
### `session.get(key)`
543480

544481
Accesses a session value from a previous request:
545482

546483
```ts
547484
session.get("name");
548485
```
549486

550-
### `session.unset()`
487+
### `session.unset(key)`
551488

552489
Removes a value from the session.
553490

554491
```ts
555492
session.unset("name");
556493
```
557494

558-
<docs-info>When using cookieSessionStorage, you must commit the session whenever you `unset`</docs-info>
559-
560-
```tsx
561-
export async function loader({
562-
request,
563-
}: LoaderFunctionArgs) {
564-
// ...
565-
566-
return json(data, {
567-
headers: {
568-
"Set-Cookie": await commitSession(session),
569-
},
570-
});
571-
}
572-
```
573-
574495
[cookies]: ./cookies
575496
[constraints]: ../guides/constraints
576497
[csrf]: https://developer.mozilla.org/en-US/docs/Glossary/CSRF

0 commit comments

Comments
 (0)