最后更新:2026-03-12

React 状态管理这个话题能吵架,Redux 党、MobX 党、Context 党、Zustand 派,各有各的道理。

我自己用过几种方案,不同项目里都有实际经验。说说我现在的看法,不是推销某一种,是根据场景给建议。

先说说什么时候需要「状态管理库」

很多人一开始就上状态管理库,其实不必要。

先问自己:这个状态需要跨多少层组件共享?

  • 只在当前组件用useState
  • 父子组件之间传 → props 就够了
  • 三四层以内的子树 → Context + useReducer
  • 跨越多个不相关的组件树,或者状态很复杂 → 状态管理库

很多中小型项目用 Context 就够了,强行上 Redux 是自找麻烦。

Context + useReducer:内置方案

对于中等复杂度的状态,不想引入第三方库时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// store/themeContext.tsx
import { createContext, useContext, useReducer } from 'react'

type Theme = 'light' | 'dark'
type Action = { type: 'TOGGLE' }

const ThemeContext = createContext<{
theme: Theme
dispatch: React.Dispatch<Action>
} | null>(null)

function themeReducer(state: Theme, action: Action): Theme {
switch (action.type) {
case 'TOGGLE':
return state === 'light' ? 'dark' : 'light'
default:
return state
}
}

export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, dispatch] = useReducer(themeReducer, 'light')

return (
<ThemeContext.Provider value={{ theme, dispatch }}>
{children}
</ThemeContext.Provider>
)
}

export function useTheme() {
const context = useContext(ThemeContext)
if (!context) throw new Error('useTheme 必须在 ThemeProvider 内使用')
return context
}

使用:

1
2
3
4
5
6
7
8
9
function ThemeToggle() {
const { theme, dispatch } = useTheme()

return (
<button onClick={() => dispatch({ type: 'TOGGLE' })}>
当前主题:{theme}
</button>
)
}

Context 的问题:任何消费这个 Context 的组件,只要 Context 值变了就会重新渲染,哪怕这个组件只用了其中一个字段。状态复杂、消费者多的时候会有性能问题。

Zustand:简单项目首选

Zustand 是我目前新项目的默认选择,没有之一。理由:

  • API 极简,5 分钟上手
  • 没有 Provider 包裹
  • 按需订阅,不订阅的部分不会触发重渲染
  • TypeScript 支持好
1
npm install zustand

基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// store/userStore.ts
import { create } from 'zustand'

interface User {
id: number
name: string
}

interface UserStore {
user: User | null
isLoading: boolean
login: (name: string) => Promise<void>
logout: () => void
}

export const useUserStore = create<UserStore>((set) => ({
user: null,
isLoading: false,

login: async (name) => {
set({ isLoading: true })
// 模拟请求
await new Promise((resolve) => setTimeout(resolve, 1000))
set({ user: { id: 1, name }, isLoading: false })
},

logout: () => {
set({ user: null })
},
}))

组件里用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Header() {
// 只订阅 user,isLoading 变化不会触发这里重渲染
const user = useUserStore((state) => state.user)
const logout = useUserStore((state) => state.logout)

if (!user) return null

return (
<header>
<span>你好,{user.name}</span>
<button onClick={logout}>退出</button>
</header>
)
}

function LoginButton() {
const isLoading = useUserStore((state) => state.isLoading)
const login = useUserStore((state) => state.login)

return (
<button onClick={() => login('Eric')} disabled={isLoading}>
{isLoading ? '登录中...' : '登录'}
</button>
)
}

注意这里的选择器写法 (state) => state.user,这样组件只在 user 变化时重渲染,isLoading 变化不影响 Header

异步操作和错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export const usePostStore = create<PostStore>((set, get) => ({
posts: [],
loading: false,
error: null,

fetchPosts: async () => {
set({ loading: true, error: null })
try {
const res = await fetch('/api/posts')
const posts = await res.json()
set({ posts, loading: false })
} catch (err) {
set({ error: '加载失败,请重试', loading: false })
}
},

// 用 get() 读取当前状态
deletePost: async (id: number) => {
const prevPosts = get().posts
// 乐观更新:先在界面删掉
set({ posts: prevPosts.filter((p) => p.id !== id) })

try {
await fetch(`/api/posts/${id}`, { method: 'DELETE' })
} catch {
// 失败了恢复
set({ posts: prevPosts })
}
},
}))

持久化存储

1
npm install zustand

用 Zustand 内置的 persist 中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

export const useSettingsStore = create(
persist(
(set) => ({
language: 'zh-CN',
theme: 'light',
setLanguage: (lang: string) => set({ language: lang }),
setTheme: (theme: string) => set({ theme }),
}),
{
name: 'app-settings', // localStorage 的 key
}
)
)

这样设置会自动同步到 localStorage,刷新页面不丢失。

Redux Toolkit:大型项目

如果你的项目很大,团队多人协作,数据流需要非常严格的规范,Redux Toolkit(RTK)是更好的选择。

RTK 是 Redux 官方维护的增强版,解决了原版 Redux 配置繁琐、模板代码太多的问题。

1
npm install @reduxjs/toolkit react-redux
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// store/postsSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// 异步 action
export const fetchPosts = createAsyncThunk('posts/fetchAll', async () => {
const res = await fetch('/api/posts')
return res.json()
})

const postsSlice = createSlice({
name: 'posts',
initialState: {
items: [] as Post[],
status: 'idle' as 'idle' | 'loading' | 'succeeded' | 'failed',
},
reducers: {
addPost(state, action) {
state.items.push(action.payload) // RTK 内部用 immer,可以直接"修改" state
},
},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.status = 'loading'
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
state.items = action.payload
})
.addCase(fetchPosts.rejected, (state) => {
state.status = 'failed'
})
},
})

export const { addPost } = postsSlice.actions
export default postsSlice.reducer

RTK 的优点是规范统一,适合大团队。缺点是模板代码多,学习成本比 Zustand 高。

我现在的选型标准

场景 推荐方案
状态只在当前组件用 useState
少量全局状态(主题、用户信息) Context 或 Zustand
中等复杂度,团队 1-5 人 Zustand
大型应用,5 人以上团队,需要严格数据流 Redux Toolkit
服务端状态(接口数据缓存) TanStack Query(推荐配合)

值得一提的是 TanStack Query(之前叫 React Query),它专门处理服务端状态——接口数据的缓存、loading 状态、失效重新请求等。跟 Zustand 配合用,前者管服务端数据,后者管客户端 UI 状态,分工很清晰。

别纠结选哪个,大部分项目 Zustand 都够用,上手快,代码干净,真的遇到瓶颈了再迁移也来得及。