nextjs-client-cookie-pattern

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Next.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 (
    'use client'
    ) can have onClick handlers
  • Only server code can set cookies (security requirement)
  • Solution: Client component calls a server action that sets cookies
该模式用于解决Next.js中的一个常见需求:通过客户端交互(如按钮点击)设置服务端Cookie
为什么需要两个文件?
  • 客户端组件(
    'use client'
    )可以拥有onClick事件处理器
  • 只有服务端代码可以设置Cookie(安全要求)
  • 解决方案:客户端组件调用Server Action来设置Cookie

The Pattern

实现模式

Scenario: A button that sets a cookie when clicked
File 1: Client Component (
app/CookieButton.tsx
)
  • Has
    'use client'
    directive
  • Has onClick handler
  • Imports and calls server action
File 2: Server Action (
app/actions.ts
)
  • Has
    'use server'
    directive
  • Uses
    cookies()
    from
    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 CookieButton
app/
├── CookieButton.tsx    ← Client component
├── actions.ts          ← Server actions
└── page.tsx            ← Uses CookieButton

TypeScript: NEVER Use
any
Type

TypeScript:绝对不要使用
any
类型

This codebase has
@typescript-eslint/no-explicit-any
enabled.
typescript
// ❌ WRONG
async function setCookie(key: any, value: any) { ... }

// ✅ CORRECT
async function setCookie(key: string, value: string) { ... }
本代码库已启用
@typescript-eslint/no-explicit-any
规则。
typescript
// ❌ 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
    'use server'
    directive
  • Import
    cookies
    from
    next/headers
  • Await
    cookies()
    (Next.js 15+)
  • 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/headers
    导入
    cookies
  • 等待
    cookies()
    返回结果(Next.js 15+)
  • 调用
    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操作的推荐方案。