リクエストとCookie
serverモードだけで使えます。ページでリクエストのヘッダーとCookieを読み、guard.tsとroute.ts、Server ActionでCookieを書けるようになります。
このページの内容
requestの読み取り
src/routes/layout.tsximport type { LayoutProps } from '@k8ordo/framework';
export default function RootLayout({
children,
request,
}: LayoutProps<'/'>) {
const theme = request.cookies.get('theme') ?? 'light';
Cookieを名前で読む
return (
<html data-theme={theme} lang="en">
<body>{children}</body>
</html>
);
}ページとレイアウト、not-found.tsxは、paramsとpathnameに加えてrequestを受け取ります。
request.headers:リクエストのヘッダーを持つHeadersです。request.cookies:CookieヘッダーをCookieの名前で引けるReadonlyMapです。同じ名前が2つあれば最初の値を使います。値は、囲む引用符を外してパーセントデコードした文字列です。
生成されるregister.gen.tsは、serverモードのときだけPagePropsとLayoutPropsにrequestを足します。staticモードでrequestを読むページは、型エラーになります。
src/components/greeting.tsximport type { RouteRequest } from '@k8ordo/framework/server';
export function Greeting({ request }: { request: RouteRequest }) {
const name = request.cookies.get('name') ?? 'there';
return <p>Hello, {name}</p>;
}子のServer Componentへrequestを渡すときは、@k8ordo/framework/serverのRouteRequestを型に使います。HeadersはClient Componentに渡せないので、Client Componentには要る値だけを取り出して渡します。requestにクエリは入りません。ページが読むクエリはsearchのexportで宣言します。宣言の仕方はパラメータを見てください。
cookies()での読み書き
src/lib/sign-in.ts'use server';
import { href } from '@k8ordo/framework';
import { cookies, redirect } from '@k8ordo/framework/server';
import { startSession } from './sessions.server';
export type SignInState = { error?: string };
export async function signIn(
_previous: SignInState,
formData: FormData,
): Promise<SignInState> {
const session = await startSession(formData);
if (session === null) return { error: 'Wrong password' };
cookies().set('session', session.token, {
`Set-Cookie`として応答に付く
maxAge: 60 * 60 * 24 * 30,
});
redirect(href('/account'));
}guard.tsとroute.ts、Server Actionの中では、@k8ordo/framework/serverのcookies()でCookieを読み書きします。上の例は、ログインに成功したらセッションのCookieを書き、/accountへリダイレクトするServer Actionです。
getは、同じリクエストの中で先にsetやdeleteした結果を反映した値を返します。guard.tsが書いた値を、そのあとに動くServer Actionが読めます。setした値は、どの応答にもSet-Cookieとして付きます。アクションのあとに再描画したページとredirect()の303、クライアントのランタイムが受け取るペイロードです。
値はSet-Cookieに書くときにパーセントエンコードされ、次のリクエストで読むときにデコードされます。setにはエンコードしていない文字列を渡します。エンコード済みの値を渡すと、二重にエンコードされます。
メモ
アクションのあとに再描画したページのrequest.cookiesには、リクエストのCookieだけが入ります。アクションが書いた値は入りません。
Cookieの属性
cookies().set('session', token, { maxAge: 3600, sameSite: 'strict' });setの3つ目の引数でCookieの属性を渡します。省略した属性は、ログインのセッションを保つCookieに合った既定値になります。
path:既定は/です。domain:既定では付きません。maxAge:有効期間を秒で渡します。0を渡すとすぐに切れます。expires:期限をDateで渡します。httpOnly:既定はtrueです。ページのスクリプトからは読めません。secure:既定はtrueです。HTTPSでしか送られません。sameSite:'strict'か'lax'、'none'のどれかです。既定は'lax'です。'none'ではlocalhostでもsecureの既定がtrueになり、secure: falseと組み合わせるとa cookie with sameSite "none" has to be secureのエラーになります。
localhostと127.0.0.1、[::1]へのHTTPのリクエストだけは、secureの既定がfalseです。ChromiumとFirefoxはそこでもSecureのCookieを保存しますが、Safariは保存しないためです。ほかのホストをHTTPで配信するなら、secure: falseを渡します。
deleteには、書いたときのpathとdomainを渡します。ブラウザはこの2つでCookieを区別します。
stateのCookie
src/routes/layout.tsximport type { LayoutProps } from '@k8ordo/framework';
import { Shell } from '../components/shell';
import { density } from '../state';
export default function RootLayout({
children,
request,
}: LayoutProps<'/'>) {
return (
<Shell initialCookie={density.parseCookies(request.cookies)}>
スキーマを通した値を1回だけ読む
{children}
</Shell>
);
}@k8ordo/stateのCookieの状態は、定義のparseCookiesにrequest.cookiesを渡して読みます。useAppStateにinitialCookieとして渡すと、サーバーでの描画からその値を使います。
initialCookieが効くのは、渡したuseAppStateだけです。レイアウトで1回読み、Client Componentへpropsで渡します。定義の仕方はCookieを見てください。
src/lib/compact.ts'use server';
import { cookies } from '@k8ordo/framework/server';
import { density } from '../state';
export async function compact() {
cookies().set(
density.cookieName,
density.cookieValue({ density: 'compact' }),
{ httpOnly: false, maxAge: 34_560_000 },
ブラウザが書くときと同じ属性
);
}同じCookieをcookies()で書くときは、名前にcookieNameを、値にcookieValue()の返り値を渡します。cookieValue()は値をスキーマに通し、渡さなかったフィールドを既定値で埋めます。maxAgeは、ブラウザが書くときと同じ400日(34_560_000秒)にします。
落とし穴
httpOnly: falseは外さないでください。HttpOnlyのCookieはスクリプトから読めないので、ブラウザのuseAppStateがその値を読めなくなります。
requestHeaders()
src/lib/report.ts'use server';
import { requestHeaders } from '@k8ordo/framework/server';
import { saveReport } from './reports.server';
export async function report(formData: FormData): Promise<void> {
const agent = requestHeaders().get('user-agent') ?? 'unknown';
リクエストのヘッダーを読む
await saveReport(formData.get('body'), agent);
}Server Actionが受け取るのは引数だけで、リクエストは受け取りません。ヘッダーはrequestHeaders()で読みます。
requestHeaders()もcookies()と同じく、guard.tsとroute.ts、Server Actionの中でだけ使えます。guard.tsとroute.tsは引数でリクエストのRequestを受け取るので、ヘッダーはふつうrequest.headersで読みます。
応答への書き込み
ページには、ステータスやSet-Cookieを書く手段がありません。書けるのはguard.tsと、POSTで呼ばれたServer Actionです。どちらもページの描画より前に動きます。ページの描画中にcookies()やresponseHeaders()を呼ぶと、belongs to what answers the requestを含むエラーになります。