Skip to content

Next.js 状态管理 🗂️

🎯 概述

状态管理是构建复杂应用的关键。Next.js 支持多种状态管理方案,包括 React Context、Zustand、Redux 等,适用于不同规模的应用。

📦 状态管理方案

1. React Context (内置)

  • 适合:小到中型应用
  • 优点:无需额外依赖
  • 缺点:性能优化较复杂

2. Zustand (推荐)

  • 适合:中到大型应用
  • 优点:简单、高性能
  • 缺点:生态相对较小

3. Redux Toolkit

  • 适合:大型企业应用
  • 优点:成熟、生态丰富
  • 缺点:学习曲线陡峭

🚀 React Context

基本用法

tsx
// context/ThemeContext.tsx
'use client'

import { createContext, useContext, useState } from 'react'

type Theme = 'light' | 'dark'

interface ThemeContextType {
  theme: Theme
  setTheme: (theme: Theme) => void
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light')
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

export function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider')
  }
  return context
}

使用:

tsx
// app/layout.tsx
import { ThemeProvider } from '@/context/ThemeContext'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

// components/ThemeToggle.tsx
'use client'

import { useTheme } from '@/context/ThemeContext'

export default function ThemeToggle() {
  const { theme, setTheme } = useTheme()
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      切换到 {theme === 'light' ? '暗色' : '亮色'} 模式
    </button>
  )
}

📝 Zustand

安装

bash
npm install zustand

创建 Store

tsx
// store/useStore.ts
import { create } from 'zustand'

interface User {
  id: string
  name: string
  email: string
}

interface StoreState {
  user: User | null
  setUser: (user: User) => void
  clearUser: () => void
  count: number
  increment: () => void
  decrement: () => void
}

export const useStore = create<StoreState>((set) => ({
  user: null,
  setUser: (user) => set({ user }),
  clearUser: () => set({ user: null }),
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}))

使用 Store

tsx
// components/Counter.tsx
'use client'

import { useStore } from '@/store/useStore'

export default function Counter() {
  const count = useStore((state) => state.count)
  const increment = useStore((state) => state.increment)
  const decrement = useStore((state) => state.decrement)
  
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
    </div>
  )
}

持久化存储

tsx
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

export const useStore = create(
  persist(
    (set) => ({
      user: null,
      setUser: (user) => set({ user }),
    }),
    {
      name: 'user-storage',
    }
  )
)

🎨 Redux Toolkit

安装

bash
npm install @reduxjs/toolkit react-redux

创建 Store

tsx
// store/store.ts
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './userSlice'
import cartReducer from './cartSlice'

export const store = configureStore({
  reducer: {
    user: userReducer,
    cart: cartReducer,
  },
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

创建 Slice

tsx
// store/userSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface User {
  id: string
  name: string
  email: string
}

interface UserState {
  user: User | null
  loading: boolean
}

const initialState: UserState = {
  user: null,
  loading: false,
}

export const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    setUser: (state, action: PayloadAction<User>) => {
      state.user = action.payload
    },
    clearUser: (state) => {
      state.user = null
    },
    setLoading: (state, action: PayloadAction<boolean>) => {
      state.loading = action.payload
    },
  },
})

export const { setUser, clearUser, setLoading } = userSlice.actions
export default userSlice.reducer

Provider 设置

tsx
// app/providers.tsx
'use client'

import { Provider } from 'react-redux'
import { store } from '@/store/store'

export function Providers({ children }: { children: React.ReactNode }) {
  return <Provider store={store}>{children}</Provider>
}

// app/layout.tsx
import { Providers } from './providers'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

使用 Redux

tsx
// components/UserProfile.tsx
'use client'

import { useSelector, useDispatch } from 'react-redux'
import { RootState } from '@/store/store'
import { setUser, clearUser } from '@/store/userSlice'

export default function UserProfile() {
  const user = useSelector((state: RootState) => state.user.user)
  const dispatch = useDispatch()
  
  const handleLogin = () => {
    dispatch(setUser({ id: '1', name: '张三', email: 'zhang@example.com' }))
  }
  
  const handleLogout = () => {
    dispatch(clearUser())
  }
  
  return (
    <div>
      {user ? (
        <div>
          <p>欢迎,{user.name}</p>
          <button onClick={handleLogout}>退出</button>
        </div>
      ) : (
        <button onClick={handleLogin}>登录</button>
      )}
    </div>
  )
}

📊 实战示例

购物车管理 (Zustand)

tsx
// store/useCartStore.ts
import { create } from 'zustand'

interface CartItem {
  id: string
  name: string
  price: number
  quantity: number
}

interface CartStore {
  items: CartItem[]
  addItem: (item: Omit<CartItem, 'quantity'>) => void
  removeItem: (id: string) => void
  updateQuantity: (id: string, quantity: number) => void
  clearCart: () => void
  total: () => number
}

export const useCartStore = create<CartStore>((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: state.items.map(i =>
      i.id === id ? { ...i, quantity } : i
    )
  })),
  
  clearCart: () => set({ items: [] }),
  
  total: () => {
    const { items } = get()
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  },
}))

使用:

tsx
// components/Cart.tsx
'use client'

import { useCartStore } from '@/store/useCartStore'

export default function Cart() {
  const items = useCartStore((state) => state.items)
  const removeItem = useCartStore((state) => state.removeItem)
  const total = useCartStore((state) => state.total())
  
  return (
    <div>
      <h2>购物车</h2>
      {items.map(item => (
        <div key={item.id}>
          <span>{item.name}</span>
          <span>{item.price}元 x {item.quantity}</span>
          <button onClick={() => removeItem(item.id)}>删除</button>
        </div>
      ))}
      <p>总计: {total}元</p>
    </div>
  )
}

用户认证 (Context)

tsx
// context/AuthContext.tsx
'use client'

import { createContext, useContext, useState, useEffect } from 'react'

interface User {
  id: string
  name: string
  email: string
}

interface AuthContextType {
  user: User | null
  login: (email: string, password: string) => Promise<void>
  logout: () => void
  loading: boolean
}

const AuthContext = createContext<AuthContextType | undefined>(undefined)

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)
  
  useEffect(() => {
    // 检查本地存储的 token
    const token = localStorage.getItem('token')
    if (token) {
      fetchUser(token)
    } else {
      setLoading(false)
    }
  }, [])
  
  const fetchUser = async (token: string) => {
    try {
      const res = await fetch('/api/user', {
        headers: { Authorization: `Bearer ${token}` }
      })
      const data = await res.json()
      setUser(data)
    } catch (error) {
      console.error('获取用户失败:', error)
    } finally {
      setLoading(false)
    }
  }
  
  const login = async (email: string, password: string) => {
    const res = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    })
    
    const { token, user } = await res.json()
    localStorage.setItem('token', token)
    setUser(user)
  }
  
  const logout = () => {
    localStorage.removeItem('token')
    setUser(null)
  }
  
  return (
    <AuthContext.Provider value={{ user, login, logout, loading }}>
      {children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return context
}

📚 最佳实践

1. 选择合适的状态管理方案

tsx
// 本地状态 - useState
function Component() {
  const [open, setOpen] = useState(false)
  return <div>{/* ... */}</div>
}

// 组件间共享 - Context
<ThemeProvider>
  <App />
</ThemeProvider>

// 全局状态 - Zustand/Redux
const user = useStore((state) => state.user)

2. 避免过度使用全局状态

tsx
// ❌ 不推荐:所有状态都放全局
const formData = useStore((state) => state.formData)

// ✅ 推荐:表单状态保持本地
const [formData, setFormData] = useState({})

3. 使用选择器优化性能

tsx
// ❌ 不推荐:订阅整个 store
const store = useStore()

// ✅ 推荐:只订阅需要的数据
const user = useStore((state) => state.user)
const count = useStore((state) => state.count)

🔗 相关资源


下一步:学习 Next.js 身份验证