React 状态管理(Zustand/Redux)
大约 9 分钟约 2678 字
React 状态管理(Zustand/Redux)
简介
React 状态管理解决组件间共享数据的难题。从简单的 useState 到全局状态库,选择合适的状态管理方案对应用架构至关重要。本篇介绍 Zustand(轻量推荐)和 Redux Toolkit(企业级)两种主流方案。
特点
Zustand
基本用法
// npm install zustand
import { create } from 'zustand'
// 定义 Store 类型
interface UserStore {
user: { id: number; name: string; email: string } | null
token: string | null
login: (username: string, password: string) => Promise<void>
logout: () => void
updateProfile: (data: Partial<User>) => void
}
// 创建 Store
const useUserStore = create<UserStore>((set, get) => ({
user: null,
token: localStorage.getItem('token'),
login: async (username, password) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
const data = await response.json()
localStorage.setItem('token', data.token)
set({ user: data.user, token: data.token })
},
logout: () => {
localStorage.removeItem('token')
set({ user: null, token: null })
},
updateProfile: (data) => {
const current = get().user
if (current) {
set({ user: { ...current, ...data } })
}
}
}))
// 组件中使用(无需 Provider)
function UserProfile() {
const user = useUserStore(state => state.user)
const logout = useUserStore(state => state.logout)
if (!user) return <div>请登录</div>
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={logout}>退出</button>
</div>
)
}
// 选择器(性能优化:只在 name 变化时重渲染)
function UserName() {
const name = useUserStore(state => state.user?.name)
return <span>{name}</span>
}带切片的 Store
// 切片模式拆分大 Store
interface CartItem {
id: number
name: string
price: number
quantity: number
}
interface CartStore {
items: CartItem[]
addItem: (item: Omit<CartItem, 'quantity'>) => void
removeItem: (id: number) => void
clearCart: () => void
total: () => number
}
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)
})),
clearCart: () => set({ items: [] }),
total: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0)
}))Zustand 中间件与 DevTools
import { create } from 'zustand'
import { devtools, subscribeWithSelector } from 'zustand/middleware'
// 组合多个中间件
const useTodoStore = create<TodoStore>()(
devtools(
subscribeWithSelector((set, get) => ({
todos: [] as Todo[],
filter: 'all' as 'all' | 'active' | 'completed',
addTodo: (text: string) => set(state => ({
todos: [...state.todos, {
id: Date.now(),
text,
completed: false
}]
}), false, 'addTodo'),
toggleTodo: (id: number) => set(state => ({
todos: state.todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
)
}), false, 'toggleTodo'),
setFilter: (filter) => set({ filter }, false, 'setFilter'),
filteredTodos: () => {
const { todos, filter } = get()
switch (filter) {
case 'active': return todos.filter(t => !t.completed)
case 'completed': return todos.filter(t => t.completed)
default: return todos
}
}
})),
{ name: 'TodoStore' } // DevTools 中的名称
)
)
// 订阅特定状态变化
useTodoStore.subscribe(
state => state.todos,
(todos) => {
console.log('Todos 变化:', todos.length)
// 可以在这里做持久化等副作用
}
)Zustand 与 React Context 对比
// 场景 1:简单跨组件状态 — useContext 足够
// 适合:主题切换、语言设置等低频变化的配置状态
// 场景 2:高频更新状态 — Zustand 更合适
// 适合:购物车、实时数据、表单状态等
// 场景 3:复杂异步流程 — Zustand 或 Redux Toolkit
// 适合:用户认证、数据同步、缓存策略
// 状态分层原则:
// 1. 组件本地状态 — useState / useReducer
// 2. 跨少数组件共享 — useContext + useReducer
// 3. 全局高频共享 — Zustand
// 4. 大型企业级应用 — Redux ToolkituseReducer 替代方案
import { useReducer } from 'react'
// 复杂本地状态管理
interface State {
count: number
step: number
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'setStep'; payload: number }
| { type: 'reset' }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step }
case 'decrement':
return { ...state, count: state.count - state.step }
case 'setStep':
return { ...state, step: action.payload }
case 'reset':
return { count: 0, step: 1 }
default:
return state
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 })
return (
<div>
<p>计数: {state.count} (步长: {state.step})</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'setStep', payload: 5 })}>
步长设为 5
</button>
<button onClick={() => dispatch({ type: 'reset' })}>重置</button>
</div>
)
}自定义 Hook 封装状态逻辑
// hooks/useAsync.ts — 通用异步状态 Hook
import { useState, useCallback, useEffect } from 'react'
interface AsyncState<T> {
data: T | null
loading: boolean
error: Error | null
}
function useAsync<T>(
asyncFn: () => Promise<T>,
deps: any[] = []
) {
const [state, setState] = useState<AsyncState<T>>({
data: null,
loading: false,
error: null,
})
const execute = useCallback(async () => {
setState(prev => ({ ...prev, loading: true, error: null }))
try {
const data = await asyncFn()
setState({ data, loading: false, error: null })
} catch (error) {
setState({ data: null, loading: false, error: error as Error })
}
}, deps)
useEffect(() => {
execute()
}, [execute])
return { ...state, refetch: execute }
}
// 使用
function UserList() {
const { data: users, loading, error, refetch } = useAsync(
() => fetch('/api/users').then(r => r.json()),
[]
)
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error.message}</div>
return (
<div>
<button onClick={refetch}>刷新</button>
<ul>
{users?.map((u: any) => <li key={u.id}>{u.name}</li>)}
</ul>
</div>
)
}持久化
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useSettingsStore = create(
persist<SettingsStore>(
(set) => ({
theme: 'light',
language: 'zh-CN',
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language })
}),
{
name: 'app-settings', // localStorage key
partialize: (state) => ({
theme: state.theme,
language: state.language
})
}
)
)Redux Toolkit
基本用法
// npm install @reduxjs/toolkit react-redux
import { createSlice, createAsyncThunk, configureStore } from '@reduxjs/toolkit'
// 异步 Thunk
export const fetchProducts = createAsyncThunk(
'products/fetchAll',
async (params: { page: number; size: number }) => {
const response = await fetch(`/api/products?page=${params.page}&size=${params.size}`)
return response.json()
}
)
// Slice
const productsSlice = createSlice({
name: 'products',
initialState: {
items: [] as Product[],
total: 0,
loading: false,
error: null as string | null
},
reducers: {
clearProducts(state) {
state.items = []
state.total = 0
}
},
extraReducers: (builder) => {
builder
.addCase(fetchProducts.pending, (state) => {
state.loading = true
state.error = null
})
.addCase(fetchProducts.fulfilled, (state, action) => {
state.loading = false
state.items = action.payload.items
state.total = action.payload.total
})
.addCase(fetchProducts.rejected, (state, action) => {
state.loading = false
state.error = action.error.message ?? '加载失败'
})
}
})
// Store
const store = configureStore({
reducer: {
products: productsSlice.reducer,
}
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
// 组件中使用
import { useSelector, useDispatch } from 'react-redux'
function ProductList() {
const dispatch = useDispatch<AppDispatch>()
const { items, loading, total } = useSelector((state: RootState) => state.products)
useEffect(() => {
dispatch(fetchProducts({ page: 1, size: 20 }))
}, [dispatch])
if (loading) return <div>加载中...</div>
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name} - ¥{item.price}</li>
))}
</ul>
)
}Redux Toolkit 高级模式
// 多 Slice 组合与 Selector 复用
// slices/authSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
export const login = createAsyncThunk(
'auth/login',
async (credentials: { username: string; password: string }) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
})
if (!res.ok) throw new Error('登录失败')
return res.json()
}
)
const authSlice = createSlice({
name: 'auth',
initialState: {
user: null as User | null,
token: null as string | null,
loading: false,
error: null as string | null,
},
reducers: {
logout: (state) => {
state.user = null
state.token = null
},
clearAuthError: (state) => {
state.error = null
},
},
extraReducers: (builder) => {
builder
.addCase(login.pending, (state) => {
state.loading = true
state.error = null
})
.addCase(login.fulfilled, (state, action) => {
state.loading = false
state.user = action.payload.user
state.token = action.payload.token
})
.addCase(login.rejected, (state, action) => {
state.loading = false
state.error = action.error.message ?? '登录失败'
})
}
})
export const { logout, clearAuthError } = authSlice.actions
// selectors — 复用选择器逻辑
export const selectCurrentUser = (state: RootState) => state.auth.user
export const selectIsAuthenticated = (state: RootState) => !!state.auth.token
export const selectAuthLoading = (state: RootState) => state.auth.loading// store/index.ts — 组合多个 Slice
import { configureStore } from '@reduxjs/toolkit'
import { authSlice } from '../slices/authSlice'
import { productsSlice } from '../slices/productsSlice'
import { cartSlice } from '../slices/cartSlice'
export const store = configureStore({
reducer: {
auth: authSlice.reducer,
products: productsSlice.reducer,
cart: cartSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false, // 如需存储非序列化数据
}),
})
// 类型安全的 Hooks
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
// 组件中使用类型安全的 Hooks
function Dashboard() {
const dispatch = useAppDispatch()
const user = useAppSelector(selectCurrentUser)
const isAuthenticated = useAppSelector(selectIsAuthenticated)
const cartItems = useAppSelector(state => state.cart.items)
}// RTK Query — 内置数据请求与缓存
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token
if (token) headers.set('Authorization', `Bearer ${token}`)
return headers
},
}),
tagTypes: ['Users', 'Products'],
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
providesTags: ['Users'],
}),
getUserById: builder.query<User, number>({
query: (id) => `/users/${id}`,
}),
createUser: builder.mutation<User, Partial<User>>({
query: (body) => ({
url: '/users',
method: 'POST',
body,
}),
invalidatesTags: ['Users'],
}),
}),
})
export const {
useGetUsersQuery,
useGetUserByIdQuery,
useCreateUserMutation,
} = apiSlice
// 组件中使用 RTK Query
function UserListPage() {
const { data: users, isLoading, error } = useGetUsersQuery()
const [createUser, { isLoading: isCreating }] = useCreateUserMutation()
if (isLoading) return <div>加载中...</div>
if (error) return <div>加载失败</div>
return (
<div>
<ul>
{users?.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
<button
onClick={() => createUser({ name: '新用户' })}
disabled={isCreating}
>
新增用户
</button>
</div>
)
}Redux 持久化
// store/persistConfig.ts
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // localStorage
import { combineReducers } from '@reduxjs/toolkit'
const persistConfig = {
key: 'root',
storage,
whitelist: ['auth', 'settings'], // 只持久化指定 reducer
// blacklist: ['products'], // 或排除指定 reducer
}
const rootReducer = combineReducers({
auth: authSlice.reducer,
products: productsSlice.reducer,
settings: settingsSlice.reducer,
})
const persistedReducer = persistReducer(persistConfig, rootReducer)
export const store = configureStore({
reducer: persistedReducer,
})
export const persistor = persistStore(store)
// App.tsx — 配合 PersistGate
import { PersistGate } from 'redux-persist/integration/react'
function App() {
return (
<Provider store={store}>
<PersistGate loading={<div>加载中...</div>} persistor={persistor}>
<Router />
</PersistGate>
</Provider>
)
}方案对比
| 特性 | Zustand | Redux Toolkit |
|---|---|---|
| 学习曲线 | 低 | 中 |
| 样板代码 | 极少 | 适中 |
| Provider | 不需要 | 需要 |
| TypeScript | 优秀 | 优秀 |
| DevTools | 有 | 强大 |
| 生态 | 中 | 丰富 |
| 适合项目 | 中小型 | 大型企业 |
优点
缺点
总结
React 状态管理推荐:中小型项目用 Zustand(轻量无 Provider),大型企业项目用 Redux Toolkit(标准 Flux 架构)。Zustand 用 create 创建 store,组件直接调用 hook。Redux Toolkit 用 createSlice 定义 reducer、createAsyncThunk 处理异步。选择原则:组件内状态用 useState,跨组件共享用 Zustand,大型项目用 Redux Toolkit。
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
- 前端主题最好同时看浏览器原理、框架机制和工程化约束。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
- 对关键页面先建立状态流和数据流,再考虑组件拆分。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
- 只追框架新特性,不分析实际渲染成本。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
- 继续补齐设计系统、SSR/边缘渲染、监控告警和组件库治理。
适用场景
- 当你准备把《React 状态管理(Zustand/Redux)》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《React 状态管理(Zustand/Redux)》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《React 状态管理(Zustand/Redux)》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《React 状态管理(Zustand/Redux)》最大的收益和代价分别是什么?
