本章目标:建立 Next.js 中状态管理的清晰心智模型——理解服务端状态 vs 客户端状态 vs URL 状态的边界,掌握 Zustand / Jotai / Redux Toolkit 的适用场景,并学会在 App Router 中正确使用全局状态。
15.1 状态分类心智模型
在 Next.js App Router 中,状态分为四类:
状态分类矩阵
状态类型 │ 存储位置 │ 工具 │ 示例
──────────────────┼─────────────────┼────────────────────┼───────────────────
服务端状态 │ 数据库 / API │ RSC + fetch + cache │ 文章、用户、评论
URL 状态 │ URL │ searchParams │ 分页、筛选、搜索
客户端全局状态 │ 浏览器内存 │ Zustand / Jotai │ 主题、购物车、通知
客户端局部状态 │ 组件内部 │ useState │ 表单输入、UI 切换
关键原则
- 服务端状态优先:能从数据库获取的数据,不要存到客户端状态
- URL 状态优先:可分享、可书签的状态,应该放在 URL 中
- 客户端全局状态最小化:只存真正需要跨组件共享的 UI 状态
- 局部状态最常用:大多数场景用
useState就够了
15.2 服务端状态(RSC + fetch)
使用 React cache 去重
// lib/services/article.ts
import { cache } from 'react';
import { prisma } from '@/lib/prisma';
// cache() 保证同一次渲染中,多次调用只执行一次查询
export const getArticle = cache(async (slug: string) => {
return prisma.article.findUnique({
where: { slug },
include: { author: true },
});
});
export const getArticles = cache(async (options: {
page?: number;
category?: string;
} = {}) => {
// ...
});
使用 revalidatePath 刷新缓存
// app/actions/article.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createArticle(formData: FormData) {
await prisma.article.create({ data: { /* ... */ } });
// 刷新列表页缓存
revalidatePath('/articles');
}
Server Component 直接获取数据
// app/articles/[slug]/page.tsx
import { getArticle } from '@/lib/services/article';
import { notFound } from 'next/navigation';
export default async function ArticlePage({
params,
}: {
params: { slug: string };
}) {
const article = await getArticle(params.slug);
if (!article) notFound();
return (
<article>
<h1>{article.title}</h1>
<p>By {article.author.name}</p>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
);
}
15.3 URL 状态
searchParams 作为状态源
// app/articles/page.tsx
import { getArticles } from '@/lib/services/article';
import Link from 'next/link';
type Props = {
searchParams: Promise<{
page?: string;
category?: string;
q?: string;
sort?: string;
}>;
};
export default async function ArticlesPage({ searchParams }: Props) {
const params = await searchParams;
const page = Number(params.page) || 1;
const category = params.category;
const search = params.q;
const sort = params.sort || 'newest';
const { articles, pagination } = await getArticles({
page,
category,
search,
sort,
});
return (
<div>
{/* 筛选器:通过 Link 修改 URL 状态 */}
<div className="flex gap-2 mb-6">
{['all', 'frontend', 'backend', 'devops'].map((cat) => (
<Link
key={cat}
href={`/articles?category=${cat === 'all' ? '' : cat}&q=${search || ''}`}
className={`px-3 py-1 rounded-full text-sm ${
(cat === 'all' && !category) || cat === category
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground'
}`}
>
{cat}
</Link>
))}
</div>
{/* 排序 */}
<div className="flex gap-4 mb-6">
<Link
href={`/articles?sort=newest&category=${category || ''}`}
className={sort === 'newest' ? 'font-bold' : ''}
>
最新
</Link>
<Link
href={`/articles?sort=popular&category=${category || ''}`}
className={sort === 'popular' ? 'font-bold' : ''}
>
最热
</Link>
</div>
{/* 文章列表 */}
<div className="space-y-4">
{articles.map((article) => (
<ArticleCard key={article.id} article={article} />
))}
</div>
{/* 分页 */}
<div className="flex gap-2 mt-8">
{pagination.hasPrev && (
<Link
href={`/articles?page=${page - 1}&category=${category || ''}`}
className="btn-outline btn-sm"
>
上一页
</Link>
)}
{pagination.hasNext && (
<Link
href={`/articles?page=${page + 1}&category=${category || ''}`}
className="btn-outline btn-sm"
>
下一页
</Link>
)}
</div>
</div>
);
}
Client Component 中操作 URL 状态
// components/article/article-search.tsx
'use client';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { useTransition, useCallback } from 'react';
import { Input } from '@/components/ui/input';
export function ArticleSearch() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [isPending, startTransition] = useTransition();
const createQueryString = useCallback(
(name: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set(name, value);
} else {
params.delete(name);
}
// 搜索时重置到第一页
if (name === 'q') {
params.set('page', '1');
}
return params.toString();
},
[searchParams]
);
return (
<Input
placeholder="搜索文章..."
defaultValue={searchParams.get('q') ?? ''}
onChange={(e) => {
startTransition(() => {
router.push(`${pathname}?${createQueryString('q', e.target.value)}`);
});
}}
className={isPending ? 'opacity-50' : ''}
/>
);
}
15.4 客户端局部状态
useState 适用场景
'use client';
import { useState } from 'react';
export function ArticleForm() {
// ✅ 表单输入
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
// ✅ UI 切换状态
const [isPreview, setIsPreview] = useState(false);
const [showAdvanced, setShowAdvanced] = useState(false);
// ✅ 临时 UI 状态
const [copied, setCopied] = useState(false);
return (
<form>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button type="button" onClick={() => setIsPreview(!isPreview)}>
{isPreview ? '编辑' : '预览'}
</button>
</form>
);
}
useReducer 适用场景
'use client';
import { useReducer } from 'react';
type State = {
items: { id: string; text: string; done: boolean }[];
filter: 'all' | 'active' | 'completed';
};
type Action =
| { type: 'ADD_ITEM'; text: string }
| { type: 'TOGGLE_ITEM'; id: string }
| { type: 'DELETE_ITEM'; id: string }
| { type: 'SET_FILTER'; filter: State['filter'] };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: [...state.items, { id: crypto.randomUUID(), text: action.text, done: false }],
};
case 'TOGGLE_ITEM':
return {
...state,
items: state.items.map((item) =>
item.id === action.id ? { ...item, done: !item.done } : item
),
};
case 'DELETE_ITEM':
return {
...state,
items: state.items.filter((item) => item.id !== action.id),
};
case 'SET_FILTER':
return { ...state, filter: action.filter };
default:
return state;
}
}
export function TodoList() {
const [state, dispatch] = useReducer(reducer, {
items: [],
filter: 'all',
});
const filtered = state.items.filter((item) => {
if (state.filter === 'active') return !item.done;
if (state.filter === 'completed') return item.done;
return true;
});
return (
<div>
{/* 筛选 */}
<div className="flex gap-2 mb-4">
{(['all', 'active', 'completed'] as const).map((f) => (
<button
key={f}
onClick={() => dispatch({ type: 'SET_FILTER', filter: f })}
className={state.filter === f ? 'font-bold' : ''}
>
{f}
</button>
))}
</div>
{/* 列表 */}
{filtered.map((item) => (
<div key={item.id} className="flex items-center gap-2">
<input
type="checkbox"
checked={item.done}
onChange={() => dispatch({ type: 'TOGGLE_ITEM', id: item.id })}
/>
<span className={item.done ? 'line-through text-gray-400' : ''}>
{item.text}
</span>
<button onClick={() => dispatch({ type: 'DELETE_ITEM', id: item.id })}>
×
</button>
</div>
))}
</div>
);
}
15.5 React Context
适用场景
Context 适合传递低频更新的全局值——主题、语言、用户信息:
// contexts/locale-context.tsx
'use client';
import { createContext, useContext, useState } from 'react';
type Locale = 'zh' | 'en';
type LocaleContextType = {
locale: Locale;
setLocale: (locale: Locale) => void;
t: (key: string) => string;
};
const translations: Record<Locale, Record<string, string>> = {
zh: { greeting: '你好', farewell: '再见' },
en: { greeting: 'Hello', farewell: 'Goodbye' },
};
const LocaleContext = createContext<LocaleContextType | undefined>(undefined);
export function LocaleProvider({ children }: { children: React.ReactNode }) {
const [locale, setLocale] = useState<Locale>('zh');
const t = (key: string) => translations[locale][key] ?? key;
return (
<LocaleContext.Provider value={{ locale, setLocale, t }}>
{children}
</LocaleContext.Provider>
);
}
export function useLocale() {
const context = useContext(LocaleContext);
if (!context) {
throw new Error('useLocale must be used within LocaleProvider');
}
return context;
}
Context 的局限
// ❌ 高频更新的值不适合用 Context(会导致所有消费者重新渲染)
const [mouseX, setMouseX] = useState(0); // 每次鼠标移动都触发全局重渲染
// ❌ 复杂状态逻辑不适合 Context(逻辑分散、难测试)
// ✅ 推荐:高频 / 复杂状态使用 Zustand 或 Jotai
15.6 Zustand(推荐方案)
安装
npm install zustand
基础 Store
// stores/cart-store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
type CartItem = {
id: string;
name: string;
price: number;
quantity: number;
image: string;
};
type CartStore = {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
totalItems: () => number;
totalPrice: () => number;
};
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((i) => i.id !== id),
})),
updateQuantity: (id, quantity) =>
set((state) => ({
items: quantity <= 0
? state.items.filter((i) => i.id !== id)
: state.items.map((i) => (i.id === id ? { ...i, quantity } : i)),
})),
clearCart: () => set({ items: [] }),
totalItems: () => get().items.reduce((sum, i) => sum + i.quantity, 0),
totalPrice: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}),
{
name: 'cart-storage', // localStorage key
storage: createJSONStorage(() => localStorage),
}
)
);
在组件中使用
// components/cart/cart-button.tsx
'use client';
import { useCartStore } from '@/stores/cart-store';
import { Button } from '@/components/ui/button';
export function CartButton() {
// ✅ 只订阅需要的值,避免不必要的重渲染
const totalItems = useCartStore((state) => state.totalItems());
return (
<Button variant="ghost" size="icon" className="relative">
🛒
{totalItems > 0 && (
<span className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-destructive text-destructive-foreground text-2xs flex items-center justify-center">
{totalItems}
</span>
)}
</Button>
);
}
// components/cart/cart-sheet.tsx
'use client';
import { useCartStore } from '@/stores/cart-store';
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
export function CartSheet({ open, onClose }: { open: boolean; onClose: () => void }) {
const items = useCartStore((state) => state.items);
const totalPrice = useCartStore((state) => state.totalPrice());
const removeItem = useCartStore((state) => state.removeItem);
const updateQuantity = useCartStore((state) => state.updateQuantity);
const clearCart = useCartStore((state) => state.clearCart);
return (
<Sheet open={open} onOpenChange={onClose}>
<SheetContent>
<SheetHeader>
<SheetTitle>购物车 ({items.length})</SheetTitle>
</SheetHeader>
<div className="mt-6 space-y-4">
{items.length === 0 ? (
<p className="text-muted-foreground text-center py-8">购物车为空</p>
) : (
<>
{items.map((item) => (
<div key={item.id} className="flex items-center gap-3">
<div className="w-12 h-12 bg-muted rounded-lg" />
<div className="flex-1">
<p className="text-sm font-medium">{item.name}</p>
<p className="text-sm text-muted-foreground">¥{item.price}</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => updateQuantity(item.id, item.quantity - 1)}
>
-
</Button>
<span className="text-sm w-6 text-center">{item.quantity}</span>
<Button
variant="outline"
size="sm"
onClick={() => updateQuantity(item.id, item.quantity + 1)}
>
+
</Button>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => removeItem(item.id)}
>
×
</Button>
</div>
))}
<div className="border-t pt-4">
<div className="flex justify-between font-semibold">
<span>合计</span>
<span>¥{totalPrice.toFixed(2)}</span>
</div>
<Button className="w-full mt-4">结算</Button>
<Button variant="ghost" className="w-full mt-2" onClick={clearCart}>
清空购物车
</Button>
</div>
</>
)}
</div>
</SheetContent>
</Sheet>
);
}
UI 状态 Store
// stores/ui-store.ts
import { create } from 'zustand';
type UIStore = {
// 侧边栏
sidebarOpen: boolean;
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
// 模态框
activeModal: string | null;
openModal: (id: string) => void;
closeModal: () => void;
// 通知
notifications: { id: string; message: string; type: 'success' | 'error' | 'info' }[];
addNotification: (message: string, type?: 'success' | 'error' | 'info') => void;
removeNotification: (id: string) => void;
};
export const useUIStore = create<UIStore>()((set) => ({
// 侧边栏
sidebarOpen: false,
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setSidebarOpen: (open) => set({ sidebarOpen: open }),
// 模态框
activeModal: null,
openModal: (id) => set({ activeModal: id }),
closeModal: () => set({ activeModal: null }),
// 通知
notifications: [],
addNotification: (message, type = 'info') =>
set((state) => ({
notifications: [
...state.notifications,
{ id: crypto.randomUUID(), message, type },
],
})),
removeNotification: (id) =>
set((state) => ({
notifications: state.notifications.filter((n) => n.id !== id),
})),
}));
异步 Store(服务端状态同步)
// stores/article-store.ts
import { create } from 'zustand';
type Article = {
id: string;
title: string;
slug: string;
excerpt: string;
};
type ArticleStore = {
articles: Article[];
loading: boolean;
error: string | null;
fetchArticles: () => Promise<void>;
optimisticAdd: (article: Article) => void;
};
export const useArticleStore = create<ArticleStore>()((set) => ({
articles: [],
loading: false,
error: null,
fetchArticles: async () => {
set({ loading: true, error: null });
try {
const res = await fetch('/api/v1/articles');
const data = await res.json();
set({ articles: data.data, loading: false });
} catch (error) {
set({ error: 'Failed to fetch articles', loading: false });
}
},
// 乐观更新
optimisticAdd: (article) =>
set((state) => ({
articles: [article, ...state.articles],
})),
}));
Zustand 最佳实践
// ✅ 选择性订阅,避免不必要的重渲染
const totalItems = useCartStore((state) => state.totalItems());
// ❌ 订阅整个 store
const store = useCartStore(); // 任何变化都会重渲染
// ✅ 使用 selector 组合
const cartSummary = useCartStore((state) => ({
total: state.totalPrice(),
count: state.totalItems(),
}));
// ✅ 在组件外部使用(如 Server Action 回调中)
import { useCartStore } from '@/stores/cart-store';
function handleClick() {
useCartStore.getState().addItem({ id: '1', name: 'Product', price: 99, image: '' });
}
15.7 Jotai(原子化状态)
安装
npm install jotai
基础 Atom
// stores/atoms.ts
import { atom } from 'jotai';
// 基础 Atom
export const countAtom = atom(0);
export const themeAtom = atom<'light' | 'dark'>('light');
// 只读派生 Atom
export const doubleCountAtom = atom((get) => get(countAtom) * 2);
// 读写派生 Atom
export const celsiusAtom = atom(0);
export const fahrenheitAtom = atom(
(get) => get(celsiusAtom) * 9 / 5 + 32,
(get, set, newValue: number) => set(celsiusAtom, (newValue - 32) * 5 / 9)
);
在组件中使用
// components/counter.tsx
'use client';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { countAtom, doubleCountAtom } from '@/stores/atoms';
export function Counter() {
const [count, setCount] = useAtom(countAtom);
const doubleCount = useAtomValue(doubleCountAtom);
return (
<div>
<p>Count: {count}</p>
<p>Double: {doubleCount}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<button onClick={() => setCount((c) => c - 1)}>-1</button>
</div>
);
}
异步 Atom
// stores/atoms/article.ts
import { atom } from 'jotai';
type Article = { id: string; title: string };
// 异步 Atom
export const articlesAtom = atom<Article[]>([]);
export const fetchArticlesAtom = atom(
null,
async (_get, set) => {
const res = await fetch('/api/v1/articles');
const data = await res.json();
set(articlesAtom, data.data);
}
);
Jotai vs Zustand 对比
| 维度 | Jotai | Zustand |
|---|---|---|
| 状态模型 | 原子化(多个独立 Atom) | 单一 Store |
| 重渲染粒度 | 极细(只重渲染使用特定 Atom 的组件) | 细(通过 selector 控制) |
| 派生状态 | 内置(atom(get)) | 需要手动(get()) |
| 持久化 | jotai-persist | zustand/middleware |
| DevTools | React DevTools | Zustand DevTools |
| 适合场景 | 分散的状态、细粒度更新 | 集中的状态、复杂逻辑 |
| 学习曲线 | 中 | 低 |
15.8 Redux Toolkit
安装
npm install @reduxjs/toolkit react-redux
配置 Store
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { cartReducer } from './features/cart-slice';
import { articleReducer } from './features/article-slice';
export const store = configureStore({
reducer: {
cart: cartReducer,
article: articleReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
创建 Slice
// store/features/cart-slice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
type CartItem = {
id: string;
name: string;
price: number;
quantity: number;
};
type CartState = {
items: CartItem[];
};
const initialState: CartState = {
items: [],
};
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem: (state, action: PayloadAction<Omit<CartItem, 'quantity'>>) => {
const existing = state.items.find((i) => i.id === action.payload.id);
if (existing) {
existing.quantity += 1;
} else {
state.items.push({ ...action.payload, quantity: 1 });
}
},
removeItem: (state, action: PayloadAction<string>) => {
state.items = state.items.filter((i) => i.id !== action.payload);
},
clearCart: (state) => {
state.items = [];
},
},
});
export const { addItem, removeItem, clearCart } = cartSlice.actions;
export const cartReducer = cartSlice.reducer;
Provider 配置
// app/providers.tsx
'use client';
import { Provider } from 'react-redux';
import { store } from '@/store';
export function Providers({ children }: { children: React.ReactNode }) {
return <Provider store={store}>{children}</Provider>;
}
使用 Typed Hooks
// store/hooks.ts
import { useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './index';
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
// components/cart.tsx
'use client';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { addItem, removeItem } from '@/store/features/cart-slice';
export function Cart() {
const dispatch = useAppDispatch();
const items = useAppSelector((state) => state.cart.items);
return (
<div>
{items.map((item) => (
<div key={item.id}>
<span>{item.name} x {item.quantity}</span>
<button onClick={() => dispatch(removeItem(item.id))}>移除</button>
</div>
))}
</div>
);
}
15.9 方案选型决策
决策矩阵
你的状态...
├── 来自服务端(数据库 / API)?
│ └── ✅ RSC + fetch + cache(不需要客户端状态管理)
│
├── 可分享 / 可书签(筛选、分页、搜索)?
│ └── ✅ URL searchParams
│
├── 只在单个组件内使用?
│ └── ✅ useState / useReducer
│
├── 低频更新的全局值(主题、语言)?
│ └── ✅ React Context
│
├── 需要跨组件共享的 UI 状态?
│ ├── 简单场景 → ✅ Zustand
│ ├── 分散的原子化状态 → ✅ Jotai
│ └── 复杂业务 / 团队已有经验 → ✅ Redux Toolkit
│
└── 需要在组件外访问(如中间件)?
└── ✅ Zustand(可在任意 JS 代码中使用)
常见场景推荐
| 场景 | 推荐方案 |
|---|---|
| 博客 / 内容站 | RSC + URL 状态(几乎不需要客户端状态) |
| SaaS 仪表盘 | Zustand(侧边栏、模态框、通知) |
| 电商购物车 | Zustand + persist(跨页面持久化) |
| 在线编辑器 | Jotai(大量分散的细粒度状态) |
| 企业后台管理 | Redux Toolkit(团队熟悉、复杂业务逻辑) |
| 社交应用 | Zustand + TanStack Query(服务端状态 + 客户端状态) |
15.10 实战:仪表盘状态管理
UI Store
// stores/dashboard-store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
type DashboardStore = {
// 侧边栏
sidebarCollapsed: boolean;
toggleSidebar: () => void;
// 选中的日期范围
dateRange: { from: string; to: string };
setDateRange: (range: { from: string; to: string }) => void;
// 表格偏好
tablePageSize: number;
setTablePageSize: (size: number) => void;
// 最近访问
recentPages: { path: string; title: string; timestamp: number }[];
addRecentPage: (page: { path: string; title: string }) => void;
};
export const useDashboardStore = create<DashboardStore>()(
persist(
(set) => ({
// 侧边栏
sidebarCollapsed: false,
toggleSidebar: () =>
set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
// 日期范围(默认最近 30 天)
dateRange: {
from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
to: new Date().toISOString(),
},
setDateRange: (range) => set({ dateRange: range }),
// 表格
tablePageSize: 20,
setTablePageSize: (size) => set({ tablePageSize: size }),
// 最近访问
recentPages: [],
addRecentPage: (page) =>
set((state) => ({
recentPages: [
{ ...page, timestamp: Date.now() },
...state.recentPages.filter((p) => p.path !== page.path),
].slice(0, 10), // 最多 10 个
})),
}),
{
name: 'dashboard-prefs',
}
)
);
在布局中使用
// app/dashboard/layout.tsx
import { DashboardSidebar } from '@/components/dashboard/sidebar';
import { DashboardHeader } from '@/components/dashboard/header';
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen">
<DashboardSidebar />
<div className="flex-1 flex flex-col">
<DashboardHeader />
<main className="flex-1 p-6">{children}</main>
</div>
</div>
);
}
// components/dashboard/sidebar.tsx
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useDashboardStore } from '@/stores/dashboard-store';
import { cn } from '@/lib/utils';
const navItems = [
{ path: '/dashboard', label: '概览', icon: '📊' },
{ path: '/dashboard/articles', label: '文章', icon: '📝' },
{ path: '/dashboard/comments', label: '评论', icon: '💬' },
{ path: '/dashboard/analytics', label: '分析', icon: '📈' },
{ path: '/dashboard/settings', label: '设置', icon: '⚙️' },
];
export function DashboardSidebar() {
const pathname = usePathname();
const collapsed = useDashboardStore((state) => state.sidebarCollapsed);
return (
<aside
className={cn(
'h-screen sticky top-0 border-r border-border bg-card transition-all duration-300',
collapsed ? 'w-16' : 'w-64'
)}
>
<div className="p-4">
<h2 className={cn('font-bold', collapsed && 'hidden')}>
Dashboard
</h2>
</div>
<nav className="space-y-1 px-2">
{navItems.map((item) => {
const isActive = pathname === item.path;
return (
<Link
key={item.path}
href={item.path}
className={cn(
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors',
isActive
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-muted'
)}
>
<span>{item.icon}</span>
{!collapsed && <span>{item.label}</span>}
</Link>
);
})}
</nav>
</aside>
);
}
本章小结
Key Takeaways
- 状态管理的首要问题是分类:服务端状态、URL 状态、全局客户端状态、局部状态
- Next.js 中大多数状态不需要客户端管理:RSC + URL 状态可以覆盖 80% 的场景
- Zustand 是 Next.js 客户端状态的首选:简单、灵活、支持 persist 和组件外访问
- Jotai 适合原子化状态:细粒度更新、派生状态方便
- Redux Toolkit 适合复杂业务:团队已有经验、大量 reducer 逻辑
- 选择性订阅是性能关键:避免订阅整个 store,只选需要的值
下一步
下一章我们将深入 表单与验证——使用 React Hook Form + Zod 构建类型安全的表单系统,涵盖动态表单、文件上传、异步验证、多步骤表单等高级场景。
参考资料
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。