Next.js State Management

Overview

State management is key to building complex applications. Next.js supports multiple state management approaches, including React Context, Zustand, Redux, and more, suitable for applications of different scales.

State Management Options

1. React Context (Built-in)

  • Best for: Small to medium applications
  • Pros: No extra dependencies
  • Cons: Performance optimization is more complex
  • Best for: Medium to large applications
  • Pros: Simple and high performance
  • Cons: Relatively smaller ecosystem

3. Redux Toolkit

  • Best for: Large enterprise applications
  • Pros: Mature with a rich ecosystem
  • Cons: Steep learning curve

React Context

Basic Usage

// 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
}

Usage:

// 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

Installation

npm install zustand

Creating a Store

// 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 })),
}))

Using the Store

// 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>
  )
}

Persistent Storage

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

Installation

npm install @reduxjs/toolkit react-redux

Creating a Store

// 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

Creating a Slice

// 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 Setup

// 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>
  )
}

Using Redux

// 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>
  )
}

Practical Examples

Shopping Cart (Zustand)

// 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)
  },
}))

Usage:

// 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>
  )
}

User Authentication (Context)

// 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
}

Best Practices

1. Choose the Right State Management Approach

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

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

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

2. Avoid Overusing Global State

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

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

3. Use Selectors for Performance

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

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

Previous Chapter: Streaming | Next Chapter: Authentication