nextjs-client-cookie-pattern について
クライアント操作からサーバー側の Cookie を設定する必要がある、Next.js App Router の開発者向けパターンガイドです。クライアントコンポーネントがボタンやフォーム操作後にサーバーアクションを呼び出します。
主な機能
- 01クリックとフォームを扱うクライアントコンポーネント
- 02サーバーアクションによる Cookie 設定
- 03next/headers による Cookie の読み書き
- 04テーマ、同意、言語設定の例
- 05Cookie のセキュリティオプション
活用シーン
- 01ボタン操作によるテーマ設定の保存
- 02Cookie 同意バナーの送信
- 03言語選択の保存
- 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 を処理する際に推奨される方法です。