状态管理:在 Next.js 中使用 React Context 或 Zustand
状态管理:在 Next.js 中使用 React Context 或 Zustand
作者:码力无边
随着应用功能的日益丰富,一个不可避免的问题摆在了我们面前:如何有效地管理和共享状态? 这里的“状态”可以是指用户的登录信息、购物车的内容、UI 的主题(暗黑/明亮模式)、表单的输入数据等等。
在传统的 React SPA 中,状态管理方案已经非常成熟,从简单的 useState + props drilling,到 React Context,再到强大的第三方库如 Redux, MobX, Zustand。
然而,Next.js App Router 和 React Server Components (RSC) 的引入,为这个经典问题带来了新的维度和挑战。Server Components 本身是无状态的,它们在服务器上渲染一次就结束了生命周期。这意味着,任何需要跨组件共享、并能响应用户交互而变化的状态,都必须存在于客户端。
这就引出了一系列新问题:
- 如何在 Server Components 和 Client Components 之间传递初始状态?
- 传统的
Context在 RSC 环境下如何工作? - 像 Redux, Zustand 这样的库,在新范式下的最佳实践是什么?
本文将为你厘清这些问题,并提供在现代 Next.js 应用中进行状态管理的实用策略和方案。
状态的分类:服务器状态 vs. 客户端状态
在 App Router 中,首先要学会区分两种类型的状态:
-
服务器状态 (Server State):
- 定义:存储在服务器端(如数据库、缓存、CMS)的数据。
- 特点:非交互式,通常是异步获取的。
- 管理:在 Server Components 中通过
async/await直接获取。Next.js 的内置fetch缓存、路由段缓存以及按需重新验证 (revalidate) 机制是管理服务器状态的核心。像 React Query 或 SWR 这样的库,虽然传统上用于客户端,但它们的核心理念(缓存、重新验证)已经被 Next.js 的数据获取层所吸收。
-
客户端状态 (Client State):
- 定义:只存在于客户端,并能响应用户交互而变化的数据。
- 特点:同步的、交互式的。
- 管理:这是本文的焦点。我们需要使用
useState,useReducer,Context或第三方库来管理。
核心原则:尽可能将状态保留在服务器上。只有当状态必须由客户端交互直接控制时,才应将其视为客户端状态。
方案一:React Context - 官方推荐的共享状态方式
对于中小型应用或特定功能模块内的状态共享,React Context 是最直接、最原生的解决方案。
挑战:Context Provider 本身需要使用 useState 或 useReducer 来持有状态值,这意味着它必须是一个客户端组件。但我们的应用大部分是由 Server Components 构成的,如何在 Server Components 树中有效地使用一个客户端的 Provider 呢?
最佳实践:将 Provider 与其消费者解耦
-
创建 Provider 组件:创建一个专门的、标记为
"use client"的组件来作为你的 Context Provider。components/theme-provider.tsx"use client"; import { createContext, useContext, useState, ReactNode } from 'react'; type Theme = 'light' | 'dark'; type ThemeContextType = { theme: Theme; toggleTheme: () => void; }; // 1. 创建 Context,可以给定一个默认值 const ThemeContext = createContext<ThemeContextType | undefined>(undefined); // 2. 创建 Provider 组件 export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<Theme>('light'); const toggleTheme = () => { setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); } // 3. 创建一个自定义 Hook,方便消费 Context export function useTheme() { const context = useContext(ThemeContext); if (context === undefined) { throw new Error('useTheme must be used within a ThemeProvider'); } return context; } -
在根布局中包裹 Provider:将这个客户端的 Provider 组件尽可能地“推高”到组件树的顶层,通常是在根
layout.tsx中。app/layout.tsximport { ThemeProvider } from '@/components/theme-provider'; export default function RootLayout({ children }: { children: React.Node }) { return ( <html> <body> <ThemeProvider> {/* ThemeProvider 是客户端组件,但它的 children 仍然可以是服务端组件! */} <Header /> {/* Server Component */} <main>{children}</main> {/* Server or Client Component */} </ThemeProvider> </body> </html> ); }关键点:虽然
ThemeProvider是一个客户端组件,但这并不意味着它的所有子组件都变成了客户端组件。React 非常智能,它允许你在一个客户端组件的“插槽”(children)中渲染服务端组件。这被称为 “Server/Client Component Interleaving”。 -
在客户端组件中消费 Context:现在,你可以在任何需要主题状态的客户端组件中使用
useThemeHook。components/theme-toggle-button.tsx"use client"; import { useTheme } from '@/components/theme-provider'; export default function ThemeToggleButton() { const { theme, toggleTheme } = useTheme(); return ( <button onClick={toggleTheme}> 切换到 {theme === 'light' ? '暗黑' : '明亮'} 模式 </button> ); }
Context 的优缺点:
- 优点:React 内置,无需额外依赖;类型安全;对于中低复杂度的状态共享非常直观。
- 缺点:当状态更新频繁或状态对象庞大时,可能会导致所有消费者组件不必要的重渲染,存在性能问题。
方案二:Zustand - 简约而强大的第三方库
当应用规模变大,或者你需要更精细的性能控制时,就需要引入专门的状态管理库了。在 RSC 时代,像 Zustand 这样轻量、简约、不依赖 Context 的库变得尤为受欢迎。
Zustand 的核心理念是使用 Hooks 来订阅状态变化的“切片 (slices)”,组件只会因为它所订阅的那部分状态变化而重渲染。
为什么 Zustand 特别适合 App Router?
- 不依赖 Provider:Zustand 的 store 是在 React 组件树之外创建的。你不需要像 Context 那样在顶部包裹一个 Provider,从而避免了“整个子树都是客户端”的担忧。
- 可在任何地方访问:你可以在任何组件(只要是客户端组件)中通过 Hook 访问 store,无需担心层级问题。
实战:使用 Zustand 管理购物车状态
-
安装 Zustand
npm install zustand -
创建 Store
store/cart-store.tsimport { create } from 'zustand'; type Product = { id: number; name: string; price: number; }; type CartState = { items: Product[]; addItem: (item: Product) => void; removeItem: (itemId: number) => void; totalItems: () => number; }; export const useCartStore = create<CartState>((set, get) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (itemId) => set((state) => ({ items: state.items.filter((item) => item.id !== itemId) })), totalItems: () => get().items.length, })); -
在客户端组件中使用 Store
components/AddToCartButton.tsx"use client"; import { useCartStore } from '@/store/cart-store'; export default function AddToCartButton({ product }) { const addItem = useCartStore((state) => state.addItem); return ( <button onClick={() => addItem(product)}> 添加到购物车 </button> ); }components/CartDisplay.tsx"use client"; import { useCartStore } from '@/store/cart-store'; export default function CartDisplay() { // 我们可以选择性地订阅状态 const totalItems = useCartStore((state) => state.totalItems()); const items = useCartStore((state) => state.items); return ( <div> <p>购物车中的商品数量: {totalItems}</p> <ul> {items.map(item => <li key={item.id}>{item.name}</li>)} </ul> </div> ); }注意:即使
items数组变化导致CartDisplay重新渲染,AddToCartButton也不会,因为它只订阅了addItem这个不会变化的函数。这就是 Zustand 的性能优势所在。
总结与选择建议
在 Next.js App Router 中,状态管理需要一种新的思维方式:明确区分服务器状态和客户端状态,并将客户端状态的管理限制在必要的范围内。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| React Context | 原生、简单、类型安全 | 存在性能隐患,Provider 必须是客户端组件 | 主题切换、用户认证信息等更新不频繁、作用域明确的全局状态。 |
| Zustand | 性能好、API 简洁、无需 Provider、体积小 | 需要引入第三方依赖 | 购物车、复杂表单、需要高性能和精细化更新的全局或局部状态。 |
给开发者的建议:
- 从 React Context 开始:对于大多数应用,先尝试使用 React 内置的 Context API。它可能已经足够满足你的需求。
- 按需引入 Zustand:当你发现 Context 带来了性能问题,或者你需要一个更结构化的、独立于组件树的状态容器时,再引入 Zustand。它的学习曲线平缓,可以与 Context 和平共存。
- 不要在客户端状态中存储服务器数据:避免将从数据库获取的数据(如文章列表)存入客户端 store。让 Next.js 的数据缓存和重新验证机制来管理这些服务器状态。
通过合理地运用这些工具,你可以在 RSC 和客户端组件和谐共存的世界里,构建出既响应迅速又易于维护的状态管理架构。
在下一篇文章中,我们将继续探讨构建复杂应用中的另一个常见环节:表单处理与校验,并学习如何使用像 React Hook Form 这样的库来简化这一过程。敬请期待!
更多推荐

所有评论(0)