mcpField
N
nextjs-client-cookie-pattern

nextjs-client-cookie-pattern

github.com/wsimmonds開発109324 回インストール53 週間前に更新
リポジトリ

nextjs-client-cookie-pattern について

クライアント操作からサーバー側の Cookie を設定する必要がある、Next.js App Router の開発者向けパターンガイドです。クライアントコンポーネントがボタンやフォーム操作後にサーバーアクションを呼び出します。

主な機能

  1. 01クリックとフォームを扱うクライアントコンポーネント
  2. 02サーバーアクションによる Cookie 設定
  3. 03next/headers による Cookie の読み書き
  4. 04テーマ、同意、言語設定の例
  5. 05Cookie のセキュリティオプション

活用シーン

  1. 01ボタン操作によるテーマ設定の保存
  2. 02Cookie 同意バナーの送信
  3. 03言語選択の保存
  4. 04Cookie 設定後のリダイレクト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wsimmonds/claude-nextjs-skills --skill nextjs-client-cookie-pattern

インストール後、Claude Code で/nextjs-client-cookie-patternを入力して読み込まれたか確認します。

詳細

Next.js:クライアントコンポーネントとサーバーアクションによる Cookie パターン

パターンの概要

このパターンでは、Next.js でよくある要件、つまり**サーバー側の Cookie 設定が必要なクライアント側の操作(ボタンのクリック)**に対応します。

なぜ 2 つのファイルが必要なのでしょうか?

  • クライアントコンポーネント('use client')では onClick ハンドラーを使用できます
  • Cookie を設定できるのはサーバーコードだけです(セキュリティ上の要件)
  • 解決方法:クライアントコンポーネントからサーバーアクションを呼び出して Cookie を設定します

パターン

シナリオ: クリックすると Cookie を設定するボタン

ファイル 1:クライアントコンポーネント(app/CookieButton.tsx)

  • 'use client' ディレクティブを指定します
  • onClick ハンドラーを設定します
  • サーバーアクションをインポートして呼び出します

ファイル 2:サーバーアクション(app/actions.ts)

  • 'use server' ディレクティブを指定します
  • next/headers の cookies() を使用します
  • Cookie を設定します

完全な実装

ファイル 1:クライアントコンポーネント
// app/CookieButton.tsx
'use client';

import { setPreference } from './actions';

export default function CookieButton() {
  const handleClick = async () => {
    await setPreference('dark-mode', 'true');
  };

  return (
    <button onClick={handleClick}>
      Enable Dark Mode
    </button>
  );
}
ファイル 2:サーバーアクション
// app/actions.ts
'use server';

import { cookies } from 'next/headers';

export async function setPreference(key: string, value: string) {
  const cookieStore = await cookies();

  cookieStore.set(key, value, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 365, // 1 year
  });
}

ファイル構成

app/
├── CookieButton.tsx    ← Client component
├── actions.ts          ← Server actions
└── page.tsx            ← Uses CookieButton

TypeScript:any 型は絶対に使用しないでください

このコードベースでは @typescript-eslint/no-explicit-any が有効になっています。

// ❌ WRONG
async function setCookie(key: any, value: any) { ... }

// ✅ CORRECT
async function setCookie(key: string, value: string) { ... }

実際の使用例

例 1:テーマの切り替え
// app/ThemeToggle.tsx
'use client';

import { useState } from 'react';
import { setTheme } from './actions';

export default function ThemeToggle() {
  const [theme, setLocalTheme] = useState('light');

  const toggle = async () => {
    const newTheme = theme === 'light' ? 'dark' : 'light';
    setLocalTheme(newTheme);
    await setTheme(newTheme);
  };

  return (
    <button onClick={toggle} className={theme}>
      {theme === 'light' ? '🌙' : '☀️'} Toggle Theme
    </button>
  );
}

// app/actions.ts
'use server';

import { cookies } from 'next/headers';

export async function setTheme(theme: 'light' | 'dark') {
  const cookieStore = await cookies();
  cookieStore.set('theme', theme, {
    httpOnly: false, // Allow client to read it
    maxAge: 60 * 60 * 24 * 365,
  });
}
例 2:Cookie 同意バナー
// app/components/CookieBanner.tsx
'use client';

import { useState } from 'react';
import { acceptCookies } from '../actions';

export default function CookieBanner() {
  const [visible, setVisible] = useState(true);

  const handleAccept = async () => {
    await acceptCookies();
    setVisible(false);
  };

  if (!visible) return null;

  return (
    <div className="cookie-banner">
      <p>We use cookies to improve your experience.</p>
      <button onClick={handleAccept}>Accept</button>
    </div>
  );
}

// app/actions.ts
'use server';

import { cookies } from 'next/headers';

export async function acceptCookies() {
  const cookieStore = await cookies();
  cookieStore.set('cookies-accepted', 'true', {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 365,
  });
}
例 3:言語設定
// app/LanguageSelector.tsx
'use client';

import { setLanguage } from './actions';

export default function LanguageSelector() {
  const languages = ['en', 'es', 'fr', 'de'];

  return (
    <select onChange={(e) => setLanguage(e.target.value)}>
      {languages.map((lang) => (
        <option key={lang} value={lang}>
          {lang.toUpperCase()}
        </option>
      ))}
    </select>
  );
}

// app/actions.ts
'use server';

import { cookies } from 'next/headers';

export async function setLanguage(lang: string) {
  const cookieStore = await cookies();
  cookieStore.set('language', lang, {
    httpOnly: false,
    maxAge: 60 * 60 * 24 * 365,
  });
}

Cookie のオプション

cookieStore.set('name', 'value', {
  httpOnly: true,    // Prevents JavaScript access (security)
  secure: true,      // Only send over HTTPS
  sameSite: 'lax',   // CSRF protection
  maxAge: 3600,      // Expires in 1 hour (seconds)
  path: '/',         // Available on all routes
});

よくあるバリエーション

フォーム送信を使用する場合
// app/PreferencesForm.tsx
'use client';

import { savePreferences } from './actions';

export default function PreferencesForm() {
  return (
    <form action={savePreferences}>
      <label>
        <input type="checkbox" name="notifications" />
        Enable Notifications
      </label>
      <button type="submit">Save</button>
    </form>
  );
}

// app/actions.ts
'use server';

import { cookies } from 'next/headers';

export async function savePreferences(formData: FormData) {
  const cookieStore = await cookies();
  const notifications = formData.get('notifications') === 'on';

  cookieStore.set('notifications', String(notifications), {
    httpOnly: true,
    maxAge: 60 * 60 * 24 * 365,
  });
}
Cookie 設定後にリダイレクトする場合
// app/actions.ts
'use server';

import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export async function login(email: string, password: string) {
  // Authenticate user
  const session = await authenticate(email, password);

  // Set session cookie
  const cookieStore = await cookies();
  cookieStore.set('session', session.token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7, // 1 week
  });

  // Redirect to dashboard
  redirect('/dashboard');
}

このパターンを使う理由

クライアントコンポーネントから直接 Cookie を設定できないのでしょうか? できません。クライアントコンポーネントはブラウザーで実行され、最新のブラウザーではセキュリティ上の理由から Cookie の操作が制限されています。サーバーアクションは、Cookie の設定が許可されているサーバー上で実行されます。

Route Handler(API ルート)を使わない理由は何ですか? 使用できます。ただし、サーバーアクションのほうがシンプルで、Next.js App Router のパターンとの統合性も高くなります。

// Alternative: Route Handler approach
// app/api/set-cookie/route.ts
export async function POST(request: Request) {
  const { name, value } = await request.json();

  return new Response(null, {
    status: 200,
    headers: {
      'Set-Cookie': `${name}=${value}; HttpOnly; Path=/; Max-Age=31536000`,
    },
  });
}

// Client component
async function setCookie() {
  await fetch('/api/set-cookie', {
    method: 'POST',
    body: JSON.stringify({ name: 'theme', value: 'dark' }),
  });
}

サーバーアクションが推奨される理由は次のとおりです。

  • 型安全性が高い
  • ボイラープレートが少ない
  • フォームとの統合性が高い
  • テストが簡単

Cookie の読み取り

サーバーコンポーネントの場合:

// app/page.tsx
import { cookies } from 'next/headers';

export default async function Page() {
  const cookieStore = await cookies();
  const theme = cookieStore.get('theme')?.value || 'light';

  return <div className={theme}>Content</div>;
}

クライアントコンポーネントの場合:

// Can't use next/headers in client components!
// Use document.cookie or a state management library
'use client';

import { useEffect, useState } from 'react';

export default function ThemeDisplay() {
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    // Read from document.cookie
    const cookieTheme = document.cookie
      .split('; ')
      .find(row => row.startsWith('theme='))
      ?.split('=')[1];

    if (cookieTheme) setTheme(cookieTheme);
  }, []);

  return <div>Current theme: {theme}</div>;
}

クイックチェックリスト

ボタンのクリックから Cookie を設定する場合:

  • 'use client' を指定したクライアントコンポーネントを作成する
  • onClick ハンドラーまたはフォーム送信を追加する
  • サーバーアクションのファイル(例:app/actions.ts)を作成する
  • 'use server' ディレクティブを追加する
  • next/headers から cookies をインポートする
  • cookies() を await する(Next.js 15+)
  • cookieStore.set(name, value, options) を呼び出す
  • クライアントコンポーネントでサーバーアクションをインポートする
  • ハンドラーからサーバーアクションを呼び出す

まとめ

クライアント・サーバー間の Cookie パターン:

  • ✅ クライアントコンポーネントがユーザー操作を処理します
  • ✅ サーバーアクションが Cookie を設定します
  • ✅ 2 つのファイルで構成します:コンポーネントとアクション
  • ✅ 適切な TypeScript による型安全性
  • ✅ 安全です(httpOnly、secure、sameSite オプション)

このパターンは、Next.js App Router でクライアント操作をきっかけに Cookie を処理する際に推奨される方法です。

コメント