@k8ordo/framework

リクエストとCookie

serverモードだけで使えます。ページでリクエストのヘッダーとCookieを読み、guard.tsとroute.ts、Server ActionでCookieを書けるようになります。

このページの内容

requestの読み取り

src/routes/layout.tsx
import 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.tsx
import 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の属性

ts
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.tsx
import 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を含むエラーになります。

k8ordo

Baselineに入った機能を、制限なく使うReactのライブラリ群。

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2