nextjs-client-cookie-pattern
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNext.js: Client Component + Server Action Cookie Pattern
Next.js:客户端组件 + Server Action Cookie 实现模式
Pattern Overview
模式概述
This pattern handles a common Next.js requirement: client-side interaction (button click) that needs to set server-side cookies.
Why Two Files?
- Client components () can have onClick handlers
'use client' - Only server code can set cookies (security requirement)
- Solution: Client component calls a server action that sets cookies
该模式用于解决Next.js中的一个常见需求:通过客户端交互(如按钮点击)设置服务端Cookie。
为什么需要两个文件?
- 客户端组件()可以拥有onClick事件处理器
'use client' - 只有服务端代码可以设置Cookie(安全要求)
- 解决方案:客户端组件调用Server Action来设置Cookie
The Pattern
实现模式
Scenario: A button that sets a cookie when clicked
File 1: Client Component ()
app/CookieButton.tsx- Has directive
'use client' - Has onClick handler
- Imports and calls server action
File 2: Server Action ()
app/actions.ts- Has directive
'use server' - Uses from
cookies()next/headers - Sets the cookie
场景:点击按钮设置Cookie
文件1:客户端组件()
app/CookieButton.tsx- 包含指令
'use client' - 拥有onClick事件处理器
- 导入并调用Server Action
文件2:Server Action()
app/actions.ts- 包含指令
'use server' - 使用中的
next/headers方法cookies() - 执行Cookie设置操作
Complete Implementation
完整实现代码
File 1: Client Component
文件1:客户端组件
typescript
// 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>
);
}typescript
// 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>
);
}File 2: Server Action
文件2:Server Action
typescript
// 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
});
}typescript
// 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
});
}File Structure
文件结构
app/
├── CookieButton.tsx ← Client component
├── actions.ts ← Server actions
└── page.tsx ← Uses CookieButtonapp/
├── CookieButton.tsx ← Client component
├── actions.ts ← Server actions
└── page.tsx ← Uses CookieButtonTypeScript: NEVER Use any
Type
anyTypeScript:绝对不要使用any
类型
anyThis codebase has enabled.
@typescript-eslint/no-explicit-anytypescript
// ❌ WRONG
async function setCookie(key: any, value: any) { ... }
// ✅ CORRECT
async function setCookie(key: string, value: string) { ... }本代码库已启用规则。
@typescript-eslint/no-explicit-anytypescript
// ❌ WRONG
async function setCookie(key: any, value: any) { ... }
// ✅ CORRECT
async function setCookie(key: string, value: string) { ... }Real-World Examples
实际场景示例
Example 1: Theme Toggle
示例1:主题切换
typescript
// 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,
});
}typescript
// 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,
});
}Example 2: Accept Cookies Banner
示例2:Cookie同意横幅
typescript
// 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,
});
}typescript
// 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,
});
}Example 3: Language Preference
示例3:语言偏好设置
typescript
// 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,
});
}typescript
// 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 Options
Cookie配置选项
typescript
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
});typescript
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
});Common Variations
常见变体
With Form Submission
结合表单提交
typescript
// 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,
});
}typescript
// 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,
});
}With Redirect After Setting Cookie
设置Cookie后重定向
typescript
// 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');
}typescript
// 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');
}Why This Pattern?
为什么选择该模式?
Can't client components set cookies directly?
No. Client components run in the browser, and modern browsers restrict cookie manipulation for security. Server actions run on the server where cookie-setting is allowed.
Why not use a Route Handler (API route)?
You can! But server actions are simpler and more integrated with the Next.js App Router pattern.
typescript
// 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' }),
});
}Server actions are preferred because they're:
- More type-safe
- Less boilerplate
- Better integrated with forms
- Easier to test
客户端组件不能直接设置Cookie吗?
不行。客户端组件运行在浏览器中,现代浏览器出于安全考虑限制了对Cookie的操作权限。而Server Action运行在服务端,允许设置Cookie。
为什么不使用路由处理器(API路由)?
当然可以!但Server Action更简单,且与Next.js App Router模式的集成度更高。
typescript
// 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' }),
});
}优先选择Server Action是因为它:
- 类型更安全
- 代码冗余更少
- 与表单的集成度更高
- 更易于测试
Reading Cookies
读取Cookie
In Server Components:
typescript
// 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>;
}In Client Components:
typescript
// 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>;
}在服务端组件中:
typescript
// 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>;
}在客户端组件中:
typescript
// 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>;
}Quick Checklist
快速检查清单
When you need to set cookies from a button click:
- Create client component with
'use client' - Add onClick handler or form submission
- Create server action file (e.g., )
app/actions.ts - Add directive
'use server' - Import from
cookiesnext/headers - Await (Next.js 15+)
cookies() - Call
cookieStore.set(name, value, options) - Import server action in client component
- Call server action from handler
当你需要通过按钮点击设置Cookie时:
- 创建包含指令的客户端组件
'use client' - 添加onClick事件处理器或表单提交逻辑
- 创建Server Action文件(例如)
app/actions.ts - 添加指令
'use server' - 从导入
next/headerscookies - 等待返回结果(Next.js 15+)
cookies() - 调用
cookieStore.set(name, value, options) - 在客户端组件中导入Server Action
- 在事件处理器中调用Server Action
Summary
总结
Client-Server Cookie Pattern:
- ✅ Client component handles user interaction
- ✅ Server action sets the cookie
- ✅ Two files: component + actions
- ✅ Type-safe with proper TypeScript
- ✅ Secure (httpOnly, secure, sameSite options)
This pattern is the recommended way to handle client-triggered cookie operations in Next.js App Router.
客户端-服务端Cookie实现模式:
- ✅ 客户端组件处理用户交互
- ✅ Server Action设置Cookie
- ✅ 双文件结构:组件 + 操作文件
- ✅ 结合TypeScript实现类型安全
- ✅ 安全可靠(支持httpOnly、secure、sameSite等配置选项)
该模式是Next.js App Router中处理客户端触发Cookie操作的推荐方案。