クッキー

クッキーは、サーバーがHTTPレスポンスでクライアントに送信する小さな情報で、ブラウザは後続のリクエストでサーバーに送信します。この技術は、多くのインタラクティブなWebサイトの基本的な構成要素であり、状態を追加することで、認証(セッション参照)、ショッピングカート、ユーザー設定、および「ログイン」しているユーザーを記憶する必要があるその他の多くの機能を構築できます。

RemixのCookieインターフェースは、クッキーのメタデータのための論理的で再利用可能なコンテナを提供します。

クッキーの使用

手動でこれらのクッキーを作成することもできますが、セッションストレージを使用する方が一般的です。

Remixでは、通常、loader関数またはaction関数(ミューテーションを参照)でクッキーを操作します。これらの関数は、データの読み書きが必要な場所です。

たとえば、電子商取引サイトに、現在セール中の商品をチェックするようにユーザーに促すバナーがあるとします。バナーはホームページの上部に表示され、ユーザーが少なくとも1週間は表示されないように、バナーを非表示にするボタンが横に付いています。

まず、クッキーを作成します。

app/cookies.server.ts
import { createCookie } from "@remix-run/node"; // or cloudflare/deno
 
export const userPrefs = createCookie("user-prefs", {
  maxAge: 604_800, // 1週間
});

次に、クッキーをimportし、loader関数またはaction関数で使用できます。この場合のloaderは、ユーザー設定の値をチェックするだけで、コンポーネントで使用してバナーを表示するかどうかを判断します。ボタンをクリックすると、<form>はサーバー上のactionを呼び出し、バナーなしでページを再読み込みします。

注: 現時点では、アプリに必要なすべてのクッキーを*.server.tsファイルに作成し、ルートモジュールにimportすることをお勧めします。これにより、Remixコンパイラは、これらのimportをブラウザビルドから正しく削除できるため、ブラウザビルドでは必要ありません。この注意書きは、今後削除される予定です。

app/routes/_index.tsx
import type {
  ActionFunctionArgs,
  LoaderFunctionArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { json, redirect } from "@remix-run/node"; // or cloudflare/deno
import {
  useLoaderData,
  Link,
  Form,
} from "@remix-run/react";
 
import { userPrefs } from "~/cookies.server";
 
export async function loader({
  request,
}: LoaderFunctionArgs) {
  const cookieHeader = request.headers.get("Cookie");
  const cookie =
    (await userPrefs.parse(cookieHeader)) || {};
  return json({ showBanner: cookie.showBanner });
}
 
export async function action({
  request,
}: ActionFunctionArgs) {
  const cookieHeader = request.headers.get("Cookie");
  const cookie =
    (await userPrefs.parse(cookieHeader)) || {};
  const bodyParams = await request.formData();
 
  if (bodyParams.get("bannerVisibility") === "hidden") {
    cookie.showBanner = false;
  }
 
  return redirect("/", {
    headers: {
      "Set-Cookie": await userPrefs.serialize(cookie),
    },
  });
}
 
export default function Home() {
  const { showBanner } = useLoaderData<typeof loader>();
 
  return (
    <div>
      {showBanner ? (
        <div>
          <Link to="/sale">セールをお見逃しなく!</Link>
          <Form method="post">
            <input
              type="hidden"
              name="bannerVisibility"
              value="hidden"
            />
            <button type="submit">非表示</button>
          </Form>
        </div>
      ) : null}
      <h1>ようこそ!</h1>
    </div>
  );
}

クッキー属性

クッキーには、有効期限、アクセス方法、送信先を制御するいくつかの属性があります。これらの属性は、createCookie(name, options)で指定するか、Set-Cookieヘッダーが生成されるときのserialize()中に指定することができます。

const cookie = createCookie("user-prefs", {
  // これは、このクッキーのデフォルトです。
  path: "/",
  sameSite: "lax",
  httpOnly: true,
  secure: true,
  expires: new Date(Date.now() + 60_000),
  maxAge: 60,
});
 
// デフォルトを使用することもできます。
cookie.serialize(userPrefs);
 
// または、必要に応じて個々の属性をオーバーライドすることもできます。
cookie.serialize(userPrefs, { sameSite: "strict" });

これらの属性の詳細については、属性に関する詳細情報を参照して、これらの属性がどのように機能するかを理解してください。

クッキーの署名

クッキーのコンテンツを受け取ったときに自動的に検証できるように、クッキーに署名することができます。HTTPヘッダーを偽造することは比較的簡単であるため、認証情報(セッションを参照)など、偽造したくない情報は署名することをお勧めします。

クッキーに署名するには、最初にクッキーを作成するときに、1つ以上のsecretsを指定します。

const cookie = createCookie("user-prefs", {
  secrets: ["s3cret1"],
});

1つ以上のsecretsを持つクッキーは、クッキーの整合性を確保する方法で保存および検証されます。

secrets配列の先頭に新しいsecretsを追加することで、secretsをローテーションできます。古いsecretsで署名されたクッキーは、cookie.parse()で引き続き正常にデコードされ、最新のsecrets(配列の最初のsecrets)は、cookie.serialize()で作成される送信されるクッキーに常に使用されます。

app/cookies.server.ts
export const cookie = createCookie("user-prefs", {
  secrets: ["n3wsecr3t", "olds3cret"],
});
app/routes/route.tsx
import { cookie } from "~/cookies.server";
 
export async function loader({
  request,
}: LoaderFunctionArgs) {
  const oldCookie = request.headers.get("Cookie");
  // oldCookieは「olds3cret」で署名されている可能性がありますが、それでも正常に解析されます。
  const value = await cookie.parse(oldCookie);
 
  new Response("...", {
    headers: {
      // Set-Cookieは「n3wsecr3t」で署名されています。
      "Set-Cookie": await cookie.serialize(value),
    },
  });
}

createCookie

サーバーからブラウザクッキーを管理するための論理的なコンテナを作成します。

import { createCookie } from "@remix-run/node"; // or cloudflare/deno
 
const cookie = createCookie("cookie-name", {
  // これらはすべて、実行時にオーバーライドできるオプションのデフォルトです。
  expires: new Date(Date.now() + 60_000),
  httpOnly: true,
  maxAge: 60,
  path: "/",
  sameSite: "lax",
  secrets: ["s3cret1"],
  secure: true,
});

各属性の詳細については、MDN Set-Cookieドキュメントを参照してください。

isCookie

オブジェクトがRemixクッキーコンテナである場合はtrueを返します。

import { isCookie } from "@remix-run/node"; // or cloudflare/deno
const cookie = createCookie("user-prefs");
console.log(isCookie(cookie));
// true

クッキーAPI

クッキーコンテナは、createCookieから返され、いくつかのプロパティとメソッドを持っています。

const cookie = createCookie(name);
cookie.name;
cookie.parse();
// etc.

cookie.name

CookieSet-CookieのHTTPヘッダーで使用されるクッキーの名前。

cookie.parse()

指定されたCookieヘッダーからこのクッキーの値を抽出して返します。

const value = await cookie.parse(
  request.headers.get("Cookie")
);

cookie.serialize()

値をシリアライズし、このクッキーのオプションと組み合わせてSet-Cookieヘッダーを作成します。これは、送信されるResponseで使用できます。

new Response("...", {
  headers: {
    "Set-Cookie": await cookie.serialize({
      showBanner: true,
    }),
  },
});

cookie.isSigned

クッキーがsecretsを使用している場合はtrue、それ以外の場合はfalseになります。

let cookie = createCookie("user-prefs");
console.log(cookie.isSigned); // false
 
cookie = createCookie("user-prefs", {
  secrets: ["soopersekrit"],
});
console.log(cookie.isSigned); // true

cookie.expires

このクッキーが期限切れになるDate。クッキーにmaxAgeexpiresの両方がある場合、この値は、Max-AgeExpiresよりも優先されるため、現在の時刻からmaxAgeの値を加えた日時になります。

const cookie = createCookie("user-prefs", {
  expires: new Date("2021-01-01"),
});
 
console.log(cookie.expires); // "2020-01-01T00:00:00.000Z"