mcpField
N
nextjs-client-cookie-pattern

nextjs-client-cookie-pattern

github.com/wsimmonds开发109324 次安装53 周前更新
代码仓库

关于 nextjs-client-cookie-pattern

面向 Next.js App Router 开发者的客户端交互与服务器端 Cookie 设置模式指南。通过客户端组件调用服务器操作,在按钮或表单操作后设置 Cookie。

主要功能

  1. 01客户端组件处理点击与表单
  2. 02服务器操作设置 Cookie
  3. 03使用 next/headers 读写 Cookie
  4. 04提供主题、同意和语言偏好示例
  5. 05说明 Cookie 安全选项

适用场景

  1. 01点击按钮保存主题偏好
  2. 02提交 Cookie 同意横幅
  3. 03保存语言选择
  4. 04设置 Cookie 后执行重定向

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/nextjs-client-cookie-pattern确认已加载。

详细介绍

Next.js:客户端组件 + Server Action Cookie 模式

模式概述

此模式用于满足常见的 Next.js 需求:客户端交互(点击按钮)需要设置服务端 Cookie。

为什么需要两个文件?

  • 客户端组件('use client')可以使用 onClick 处理程序
  • 只有服务端代码才能设置 Cookie(安全要求)
  • 解决方案:客户端组件调用用于设置 Cookie 的 Server Action

此模式

**场景:**点击按钮时设置 Cookie

文件 1:客户端组件(app/CookieButton.tsx)

  • 包含 'use client' 指令
  • 包含 onClick 处理程序
  • 导入并调用 Server Action

文件 2:Server Action(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:Server Action
// 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 操作。Server Action 在允许设置 Cookie 的服务端运行。

为什么不使用 Route Handler(API 路由)? 可以!但 Server Action 更简单,也与 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' }),
  });
}

推荐使用 Server Action,因为它们:

  • 类型安全性更高
  • 样板代码更少
  • 与表单的集成更好
  • 更容易测试

读取 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 处理程序或表单提交
  • 创建 Server Action 文件(例如 app/actions.ts)
  • 添加 'use server' 指令
  • 从 next/headers 导入 cookies
  • 等待 cookies() 完成(Next.js 15+)
  • 调用 cookieStore.set(name, value, options)
  • 在客户端组件中导入 Server Action
  • 在处理程序中调用 Server Action

总结

客户端-服务端 Cookie 模式:

  • ✅ 客户端组件处理用户交互
  • ✅ Server Action 设置 Cookie
  • ✅ 两个文件:组件 + actions
  • ✅ 使用正确的 TypeScript,确保类型安全
  • ✅ 安全(httpOnly、secure、sameSite 选项)

此模式是 Next.js App Router 中处理由客户端触发的 Cookie 操作的推荐方式。

评论

支持 Markdown