跳到主要内容

1 篇博文 含有标签「状态库」

查看所有标签

状态库之殇

· 阅读需 27 分钟
Mike

在 React 生态中,状态管理是一个经久不衰的话题。从最初的 Flux 到后来的 Redux,再到现在的 Zustand、Jotai、Valtio,工具在变,但核心痛点似乎从未消失:API 繁琐、性能陷阱、心智负担

本文将通过一个高度复杂的业务场景,深度剖析各大状态库的设计哲学与局限性。不是简单的 Counter Demo,而是真实业务中你 一定会遇到 的复杂度。


一个真实的复杂状态场景

为了对比,我们设计一个 "项目协作平台" 的完整状态——包含工作区、看板、阶段、任务、子任务、用户、UI 状态、以及 WebSocket 连接状态:

// 完整的 State 类型定义
interface AppState {
// 🏢 工作区 → 看板 → 阶段 → 任务 → 子任务(5 层嵌套)
workspaces: Array<{
id: string
name: string
members: string[]
boards: Array<{
id: string
title: string
stages: Array<{
id: string
title: string
color: string
tasks: Array<{
id: string
title: string
description: string
assigneeId: string | null
priority: 'low' | 'medium' | 'high' | 'urgent'
tags: string[]
done: boolean
dueDate: string | null
subtasks: Array<{
id: string
text: string
done: boolean
}>
comments: Array<{
id: string
userId: string
content: string
createdAt: number
}>
}>
}>
}>
}>

// 👤 用户表(扁平化)
users: Record<string, {
id: string
name: string
avatar: string
online: boolean
}>

// 🖥️ UI 状态
ui: {
activeWorkspaceId: string | null
activeBoardId: string | null
sidebarCollapsed: boolean
taskDetailModal: {
visible: boolean
taskId: string | null
}
searchQuery: string
filters: {
priority: string[]
assignee: string[]
tags: string[]
}
}

// 🔌 连接状态
connection: {
status: 'connected' | 'disconnected' | 'reconnecting'
lastSyncAt: number | null
}
}

核心业务操作

我们需要实现以下几个 真实业务场景 的操作:

  1. 深层更新:修改某个工作区 → 某个看板 → 某个阶段 → 某个任务的标题
  2. 跨层关联:完成任务时,自动检查子任务是否全部完成
  3. 按需订阅:任务列表组件只关心当前阶段的 tasks,不关心其他数据变化
  4. 异步副作用:任务更新后同步到服务器,失败时回滚
  5. 状态持久化:将 UI 偏好持久化到 localStorage

Zustand:简约外表下的深层嵌套地狱

Store 定义:Spread 地狱

import { create } from 'zustand'
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

// 🔥 痛点1:多中间件嵌套,括号套括号,类型推导经常丢失
const useProjectStore = create(
devtools(
persist(
subscribeWithSelector(
immer((set, get) => ({
workspaces: [],
users: {},
ui: {
activeWorkspaceId: null,
activeBoardId: null,
sidebarCollapsed: false,
taskDetailModal: { visible: false, taskId: null },
searchQuery: '',
filters: { priority: [], assignee: [], tags: [] },
},
connection: { status: 'disconnected', lastSyncAt: null },

// ---- Actions ----

// 🔥 痛点2:即使用了 immer,5 层嵌套的查找逻辑依然冗长
updateTaskTitle: (wsId, boardId, stageId, taskId, newTitle) =>
set((state) => {
const ws = state.workspaces.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const stage = board?.stages.find((s) => s.id === stageId)
const task = stage?.tasks.find((t) => t.id === taskId)
if (task) task.title = newTitle
}),

// 完成任务
completeTask: (wsId, boardId, stageId, taskId) =>
set((state) => {
const ws = state.workspaces.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const stage = board?.stages.find((s) => s.id === stageId)
const task = stage?.tasks.find((t) => t.id === taskId)
if (task) {
task.done = true
task.tags.push('system-completed')
// 同时标记所有子任务为完成
task.subtasks.forEach((st) => (st.done = true))
}
}),

// 移动任务到另一个阶段
moveTask: (wsId, boardId, fromStageId, toStageId, taskId) =>
set((state) => {
const ws = state.workspaces.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const fromStage = board?.stages.find((s) => s.id === fromStageId)
const toStage = board?.stages.find((s) => s.id === toStageId)
if (fromStage && toStage) {
const taskIndex = fromStage.tasks.findIndex(
(t) => t.id === taskId
)
if (taskIndex > -1) {
const [task] = fromStage.tasks.splice(taskIndex, 1)
toStage.tasks.push(task)
}
}
}),

// 🔥 痛点3:异步操作需要手动处理 loading、error、回滚
syncTaskToServer: async (wsId, boardId, stageId, taskId) => {
const state = get()
const ws = state.workspaces.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const stage = board?.stages.find((s) => s.id === stageId)
const task = stage?.tasks.find((t) => t.id === taskId)
try {
await fetch(`/api/tasks/${taskId}`, {
method: 'PUT',
body: JSON.stringify(task),
})
set((state) => {
state.connection.lastSyncAt = Date.now()
})
} catch (err) {
// 失败了怎么回滚?要自己存快照...
console.error('同步失败,需要手动实现回滚逻辑')
}
},

// 更新搜索
setSearchQuery: (query) =>
set((state) => {
state.ui.searchQuery = query
}),

// 切换侧边栏
toggleSidebar: () =>
set((state) => {
state.ui.sidebarCollapsed = !state.ui.sidebarCollapsed
}),
}))
),
{ name: 'project-store' } // persist 配置
),
{ name: 'ProjectStore' } // devtools 配置
)
) // 🔥 数一数,光是末尾就有多少个闭合括号?

组件消费:Selector 地狱

function TaskBoard({ wsId, boardId }) {
// 🔥 痛点4:每个数据都要写一个 selector,否则任何状态变化都会触发重渲染
const stages = useProjectStore(
(s) => s.workspaces
.find((w) => w.id === wsId)
?.boards.find((b) => b.id === boardId)
?.stages,
// 🔥 还必须传 shallow 比较,否则每次都是新引用
shallow
)

const searchQuery = useProjectStore((s) => s.ui.searchQuery)
const filters = useProjectStore((s) => s.ui.filters, shallow)
const updateTaskTitle = useProjectStore((s) => s.updateTaskTitle)
const completeTask = useProjectStore((s) => s.completeTask)
const moveTask = useProjectStore((s) => s.moveTask)
// 🔥 每个 action 也要单独 select 出来...

// 🔥 痛点5:派生状态要自己算,且没有缓存(除非再引入 reselect)
const filteredTasks = useMemo(() => {
return stages?.flatMap((stage) =>
stage.tasks.filter((task) => {
if (searchQuery && !task.title.includes(searchQuery)) return false
if (filters.priority.length && !filters.priority.includes(task.priority))
return false
if (filters.assignee.length && !filters.assignee.includes(task.assigneeId))
return false
return true
})
)
}, [stages, searchQuery, filters])

// ...渲染逻辑
}

Zustand 结论

API 繁琐度:⭐⭐⭐⭐

痛点说明
中间件嵌套devtools(persist(subscribeWithSelector(immer(...)))) 括号地狱
Selector 样板每个字段都需要手写 selector + shallow 比较
深层更新即使有 immer,5 层 .find() 查找链依然冗长
异步处理没有内置方案,错误处理和回滚完全手动
派生状态无内置 computed,需要自己 useMemo 或引入 reselect

Redux:Action、Reducer、Slice、Middleware,模板代码堆成山

第一步:定义 Types(纯 Redux 的噩梦)

// 🔥 痛点1:即使用 RTK,你也逃不过 action payload 的类型定义
// 如果是纯 Redux,你需要手动定义每一个 Action Type...

// actionTypes.ts
export const UPDATE_TASK_TITLE = 'project/updateTaskTitle'
export const COMPLETE_TASK = 'project/completeTask'
export const MOVE_TASK = 'project/moveTask'
export const SET_SEARCH_QUERY = 'ui/setSearchQuery'
export const TOGGLE_SIDEBAR = 'ui/toggleSidebar'
export const SYNC_TASK_START = 'sync/syncTaskStart'
export const SYNC_TASK_SUCCESS = 'sync/syncTaskSuccess'
export const SYNC_TASK_FAILURE = 'sync/syncTaskFailure'
// ... 每新增一个操作就加一行

第二步:RTK Slice 定义

// projectSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// 🔥 痛点2:异步操作必须用 createAsyncThunk,又是一层包装
export const syncTaskToServer = createAsyncThunk(
'project/syncTask',
async ({ wsId, boardId, stageId, taskId }, { getState, rejectWithValue }) => {
const state = getState()
// 🔥 从 store 里取嵌套数据?又是一串 find...
const ws = state.project.workspaces.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const stage = board?.stages.find((s) => s.id === stageId)
const task = stage?.tasks.find((t) => t.id === taskId)
try {
const res = await fetch(`/api/tasks/${taskId}`, {
method: 'PUT',
body: JSON.stringify(task),
})
return await res.json()
} catch (err) {
return rejectWithValue(err.message)
}
}
)

const projectSlice = createSlice({
name: 'project',
initialState: {
workspaces: [],
syncStatus: 'idle', // 🔥 还要额外维护同步状态
syncError: null,
},
reducers: {
updateTaskTitle: (state, action) => {
const { wsId, boardId, stageId, taskId, newTitle } = action.payload
const ws = state.workspaces.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const stage = board?.stages.find((s) => s.id === stageId)
const task = stage?.tasks.find((t) => t.id === taskId)
if (task) task.title = newTitle
},

completeTask: (state, action) => {
const { wsId, boardId, stageId, taskId } = action.payload
const ws = state.workspaces.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const stage = board?.stages.find((s) => s.id === stageId)
const task = stage?.tasks.find((t) => t.id === taskId)
if (task) {
task.done = true
task.tags.push('system-completed')
task.subtasks.forEach((st) => (st.done = true))
}
},

moveTask: (state, action) => {
const { wsId, boardId, fromStageId, toStageId, taskId } = action.payload
const ws = state.workspaces.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const fromStage = board?.stages.find((s) => s.id === fromStageId)
const toStage = board?.stages.find((s) => s.id === toStageId)
if (fromStage && toStage) {
const idx = fromStage.tasks.findIndex((t) => t.id === taskId)
if (idx > -1) {
const [task] = fromStage.tasks.splice(idx, 1)
toStage.tasks.push(task)
}
}
},
},

// 🔥 痛点3:extraReducers 处理异步的三种状态,每个 thunk 都要写三遍
extraReducers: (builder) => {
builder
.addCase(syncTaskToServer.pending, (state) => {
state.syncStatus = 'loading'
state.syncError = null
})
.addCase(syncTaskToServer.fulfilled, (state) => {
state.syncStatus = 'succeeded'
})
.addCase(syncTaskToServer.rejected, (state, action) => {
state.syncStatus = 'failed'
state.syncError = action.payload
})
},
})

第三步:还需要 UI Slice、Connection Slice...

// uiSlice.js — 🔥 痛点4:状态按"领域"拆分成多个 Slice,互相引用极其麻烦
const uiSlice = createSlice({
name: 'ui',
initialState: {
activeWorkspaceId: null,
activeBoardId: null,
sidebarCollapsed: false,
taskDetailModal: { visible: false, taskId: null },
searchQuery: '',
filters: { priority: [], assignee: [], tags: [] },
},
reducers: {
setActiveWorkspace: (state, action) => {
state.activeWorkspaceId = action.payload
},
setActiveBoard: (state, action) => {
state.activeBoardId = action.payload
},
toggleSidebar: (state) => {
state.sidebarCollapsed = !state.sidebarCollapsed
},
openTaskDetail: (state, action) => {
state.taskDetailModal = { visible: true, taskId: action.payload }
},
closeTaskDetail: (state) => {
state.taskDetailModal = { visible: false, taskId: null }
},
setSearchQuery: (state, action) => {
state.searchQuery = action.payload
},
setFilters: (state, action) => {
state.filters = { ...state.filters, ...action.payload }
},
},
})

// connectionSlice.js
const connectionSlice = createSlice({
name: 'connection',
initialState: { status: 'disconnected', lastSyncAt: null },
reducers: {
setConnectionStatus: (state, action) => {
state.status = action.payload
},
},
// 🔥 痛点5:跨 Slice 响应——connection 想响应 syncTask 的状态?
// 必须在 extraReducers 里监听另一个 Slice 的 action
extraReducers: (builder) => {
builder.addCase(syncTaskToServer.fulfilled, (state) => {
state.lastSyncAt = Date.now()
})
},
})

第四步:组装 Store + 中间件

// store.js
import { configureStore } from '@reduxjs/toolkit'
import { createLogger } from 'redux-logger'

// 🔥 痛点6:中间件配置,每个都有自己的 API 和坑
const loggerMiddleware = createLogger({
collapsed: true,
diff: true,
})

export const store = configureStore({
reducer: {
project: projectSlice.reducer,
ui: uiSlice.reducer,
connection: connectionSlice.reducer,
// 🔥 每新增一个 Slice 就要来这里注册
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// 🔥 Date 对象不可序列化?要手动忽略...
ignoredActions: ['project/syncTask/fulfilled'],
ignoredPaths: ['connection.lastSyncAt'],
},
}).concat(loggerMiddleware),
})

// 🔥 痛点7:别忘了导出 Actions
export const {
updateTaskTitle,
completeTask,
moveTask,
} = projectSlice.actions
export const {
setActiveWorkspace,
setActiveBoard,
toggleSidebar,
openTaskDetail,
closeTaskDetail,
setSearchQuery,
setFilters,
} = uiSlice.actions
export const { setConnectionStatus } = connectionSlice.actions

第五步:组件消费

import { useSelector, useDispatch } from 'react-redux'
import { createSelector } from 'reselect'

// 🔥 痛点8:Selector 也是一座山——每个派生数据都要 createSelector 缓存
const selectFilteredTasks = createSelector(
[(state) => state.project.workspaces,
(state) => state.ui.activeBoardId,
(state) => state.ui.searchQuery,
(state) => state.ui.filters],
(workspaces, boardId, query, filters) => {
// ... 复杂的过滤逻辑
}
)

function TaskBoard() {
const dispatch = useDispatch()
const tasks = useSelector(selectFilteredTasks)
const syncStatus = useSelector((s) => s.project.syncStatus)

const handleComplete = (wsId, boardId, stageId, taskId) => {
// 🔥 痛点9:dispatch + action creator,每次调用都要包一层
dispatch(completeTask({ wsId, boardId, stageId, taskId }))
dispatch(syncTaskToServer({ wsId, boardId, stageId, taskId }))
}

return (
<div>
{syncStatus === 'loading' && <Spinner />}
{tasks.map((task) => (
<TaskCard
key={task.id}
task={task}
onComplete={() => handleComplete(/*...一堆参数*/)}
/>
))}
</div>
)
}

Redux 结论

API 繁琐度:⭐⭐⭐⭐⭐(满星)

痛点说明
文件爆炸一个功能至少涉及 Slice、Store、Actions 导出、Selector 四处代码
异步三件套每个 createAsyncThunk 都必须处理 pending/fulfilled/rejected
跨 Slice 通信只能通过 extraReducers 监听其他 Slice 的 Action,极其不直观
Selector 地狱每个派生数据都需要 createSelector 做缓存
dispatch 包装组件里每次操作都是 dispatch(actionCreator(payload))
序列化检查configureStore 默认的序列化检查会和 Date、Map 等类型冲突

Jotai:原子化带来的"死亡之碎片"

Atom 定义:一个功能拆成十几个 Atom

import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'

// 🔥 痛点1:状态要被拆成一堆 atom
const workspacesAtom = atom([])
const usersAtom = atom({})
const connectionAtom = atom({ status: 'disconnected', lastSyncAt: null })

// UI 状态 —— 每个字段一个 atom
const activeWorkspaceIdAtom = atomWithStorage('activeWsId', null)
const activeBoardIdAtom = atomWithStorage('activeBoardId', null)
const sidebarCollapsedAtom = atomWithStorage('sidebarCollapsed', false)
const taskDetailModalAtom = atom({ visible: false, taskId: null })
const searchQueryAtom = atom('')
const filtersAtom = atom({ priority: [], assignee: [], tags: [] })

// 🔥 痛点2:派生 atom,每层嵌套都需要一个 derived atom
const activeWorkspaceAtom = atom((get) => {
const workspaces = get(workspacesAtom)
const activeId = get(activeWorkspaceIdAtom)
return workspaces.find((ws) => ws.id === activeId) ?? null
})

const activeBoardAtom = atom((get) => {
const ws = get(activeWorkspaceAtom)
const boardId = get(activeBoardIdAtom)
return ws?.boards.find((b) => b.id === boardId) ?? null
})

const activeBoardStagesAtom = atom((get) => {
const board = get(activeBoardAtom)
return board?.stages ?? []
})

// 🔥 痛点3:过滤逻辑也要变成 atom
const filteredTasksAtom = atom((get) => {
const stages = get(activeBoardStagesAtom)
const query = get(searchQueryAtom)
const filters = get(filtersAtom)
return stages.flatMap((stage) =>
stage.tasks.filter((task) => {
if (query && !task.title.includes(query)) return false
if (
filters.priority.length &&
!filters.priority.includes(task.priority)
)
return false
return true
})
)
})

// 🔥 痛点4:写操作也必须是 writable atom,又是一层抽象
const updateTaskTitleAtom = atom(
null,
(get, set, { wsId, boardId, stageId, taskId, newTitle }) => {
const workspaces = get(workspacesAtom)
const newWorkspaces = workspaces.map((ws) =>
ws.id === wsId
? {
...ws,
boards: ws.boards.map((b) =>
b.id === boardId
? {
...b,
stages: b.stages.map((s) =>
s.id === stageId
? {
...s,
tasks: s.tasks.map((t) =>
t.id === taskId ? { ...t, title: newTitle } : t
),
}
: s
),
}
: b
),
}
: ws
)
set(workspacesAtom, newWorkspaces)
}
)

const completeTaskAtom = atom(
null,
(get, set, { wsId, boardId, stageId, taskId }) => {
// 🔥 又是一大段 map 嵌套...和 Zustand 一模一样的痛苦
const workspaces = get(workspacesAtom)
const newWorkspaces = workspaces.map((ws) =>
ws.id === wsId
? {
...ws,
boards: ws.boards.map((b) =>
b.id === boardId
? {
...b,
stages: b.stages.map((s) =>
s.id === stageId
? {
...s,
tasks: s.tasks.map((t) =>
t.id === taskId
? {
...t,
done: true,
tags: [...t.tags, 'system-completed'],
subtasks: t.subtasks.map((st) => ({
...st,
done: true,
})),
}
: t
),
}
: s
),
}
: b
),
}
: ws
)
set(workspacesAtom, newWorkspaces)
}
)

// 🔥 异步 atom
const syncTaskAtom = atom(null, async (get, set, { taskId }) => {
const workspaces = get(workspacesAtom)
// ... 又要从嵌套结构里 find task
try {
await fetch(`/api/tasks/${taskId}`, { method: 'PUT' })
set(connectionAtom, { status: 'connected', lastSyncAt: Date.now() })
} catch {
set(connectionAtom, (prev) => ({ ...prev, status: 'disconnected' }))
}
})

组件消费:useAtom / useAtomValue 的海洋

import { useAtom, useAtomValue, useSetAtom } from 'jotai'

function TaskBoard() {
// 🔥 痛点5:每个 atom 都要单独 hook,一个组件可以轻松写出 10+ 个 useAtom
const stages = useAtomValue(activeBoardStagesAtom)
const filteredTasks = useAtomValue(filteredTasksAtom)
const users = useAtomValue(usersAtom)
const activeWsId = useAtomValue(activeWorkspaceIdAtom)
const activeBoardId = useAtomValue(activeBoardIdAtom)
const [searchQuery, setSearchQuery] = useAtom(searchQueryAtom)
const [filters, setFilters] = useAtom(filtersAtom)
const [sidebarCollapsed, toggleSidebar] = useAtom(sidebarCollapsedAtom)
const [taskDetailModal, setTaskDetailModal] = useAtom(taskDetailModalAtom)
const updateTaskTitle = useSetAtom(updateTaskTitleAtom)
const completeTask = useSetAtom(completeTaskAtom)
const syncTask = useSetAtom(syncTaskAtom)
const connection = useAtomValue(connectionAtom)
// 🔥 光是 hook 声明就占了 15 行,组件逻辑还没开始写...

return (
<div>
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{filteredTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
assignee={users[task.assigneeId]}
onTitleChange={(newTitle) =>
updateTaskTitle({
wsId: activeWsId,
boardId: activeBoardId,
stageId: task.stageId,
taskId: task.id,
newTitle,
})
}
/>
))}
</div>
)
}

// 🔥 痛点6:子组件也要独立引入自己需要的 atom
function TaskCard({ task }) {
const users = useAtomValue(usersAtom)
const completeTask = useSetAtom(completeTaskAtom)
const [modal, setModal] = useAtom(taskDetailModalAtom)
// 🔥 atom 的导入链像病毒一样扩散到每个组件...
// ...
}

Jotai 结论

API 繁琐度:⭐⭐⭐⭐

痛点说明
Atom 碎片化一个页面的状态可以拆出 20+ 个 atom 文件
Hook 爆炸每个组件都是 useAtomValue / useSetAtom 的海洋
嵌套更新writable atom 里的更新逻辑和 Zustand 完全一样的 Spread 地狱
依赖追踪derived atom 形成隐式依赖图,debug 时看不到全貌
导入地狱每个组件要 import 一堆 atom,文件间耦合严重

Valtio:Proxy 的甜蜜陷阱

Store 定义:看起来最爽

import { proxy, useSnapshot, subscribe } from 'valtio'
import { devtools } from 'valtio/utils'

// ✅ 定义确实简单
const state = proxy({
workspaces: [],
users: {},
ui: {
activeWorkspaceId: null,
activeBoardId: null,
sidebarCollapsed: false,
taskDetailModal: { visible: false, taskId: null },
searchQuery: '',
filters: { priority: [], assignee: [], tags: [] },
},
connection: { status: 'disconnected', lastSyncAt: null },
})

// ✅ 修改操作确实直观
function updateTaskTitle(wsId, boardId, stageId, taskId, newTitle) {
const ws = state.workspaces.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const stage = board?.stages.find((s) => s.id === stageId)
const task = stage?.tasks.find((t) => t.id === taskId)
if (task) task.title = newTitle
}

function completeTask(wsId, boardId, stageId, taskId) {
const ws = state.workspaces.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const stage = board?.stages.find((s) => s.id === stageId)
const task = stage?.tasks.find((t) => t.id === taskId)
if (task) {
task.done = true
task.tags.push('system-completed')
task.subtasks.forEach((st) => (st.done = true))
}
}

组件消费:useSnapshot 的 readonly 陷阱

function TaskBoard() {
// ✅ 看起来很简洁
const snap = useSnapshot(state)

const activeWs = snap.workspaces.find(
(ws) => ws.id === snap.ui.activeWorkspaceId
)
const activeBoard = activeWs?.boards.find(
(b) => b.id === snap.ui.activeBoardId
)

return (
<div>
{/* 🔥🔥🔥 痛点1:snap 是 readonly 的深度冻结对象!
下面这行代码会在运行时报错:
TypeError: Cannot assign to read only property 'searchQuery'
*/}
<input
value={snap.ui.searchQuery}
onChange={(e) => {
// ❌ snap.ui.searchQuery = e.target.value // 报错!
// ✅ 必须操作原始 state,不是 snap
state.ui.searchQuery = e.target.value
}}
/>

{/* 🔥 痛点2:snap 和 state 的混用让人精神分裂
读数据用 snap,写数据用 state
一不小心混淆就是 bug */}
<Sidebar
collapsed={snap.ui.sidebarCollapsed} // 读:用 snap
onToggle={() => {
state.ui.sidebarCollapsed = !state.ui.sidebarCollapsed // 写:用 state
}}
/>

{activeBoard?.stages.map((stage) => (
<StageColumn key={stage.id} stage={stage} />
))}
</div>
)
}

function TaskCard({ task, wsId, boardId, stageId }) {
// 🔥 痛点3:子组件拿到的 task 是 snap 的一部分(readonly)
// 但调用 action 时需要传 id 而不能传 snap 对象
return (
<div>
<h3>{task.title}</h3>
{/* 🔥 痛点4:受控 input 与 readonly snap 的矛盾
你不能把 snap 属性直接绑到 input 的 onChange 中修改 */}
<input
value={task.title}
onChange={(e) => {
// ❌ task.title = e.target.value // 报错!task 来自 snap
// ✅ 必须穿透回原始 state
updateTaskTitle(wsId, boardId, stageId, task.id, e.target.value)
}}
/>
<button onClick={() => completeTask(wsId, boardId, stageId, task.id)}>
完成
</button>
</div>
)
}

更多陷阱

// 🔥 痛点5:Proxy 对象不能直接序列化
console.log(JSON.stringify(state))
// 输出的是 Proxy 包装后的对象,可能丢失某些属性

// 🔥 痛点6:subscribe 的粒度控制很粗糙
subscribe(state, () => {
// 任何属性变化都会触发,没有细粒度 selector
console.log('state changed')
})

// 想要细粒度?要用 subscribeKey,但它只支持顶层 key
import { subscribeKey } from 'valtio/utils'
subscribeKey(state, 'ui', () => {
// 🔥 只能监听顶层 key,不能监听 state.ui.searchQuery
})

// 🔥 痛点7:和 React.memo 配合时的引用陷阱
// snap 每次都是新对象,memo 里的浅比较会失效
const MemoizedTask = React.memo(TaskCard)
// snap.xxx 传给 memo 组件 → 每次都是新引用 → memo 失效 → 性能问题

Valtio 结论

API 繁琐度:⭐⭐(定义简单,但使用时暗坑密布)

痛点说明
readonly snapuseSnapshot 返回冻结对象,input 受控组件直接报错
snap vs state读用 snap 写用 state,认知分裂,新人必踩坑
序列化问题Proxy 对象不能直接 JSON.stringify
memo 失效snap 每次创建新引用,React.memo 浅比较失效
细粒度订阅subscribe 只能监听整个对象或顶层 key,无法深层订阅

MobX:面向对象的"Java 在 React 里借尸还魂"

Store 定义

import { makeAutoObservable, flow, reaction, autorun } from 'mobx'

class ProjectStore {
workspaces = []
users = {}
syncStatus = 'idle'

constructor() {
// 🔥 痛点1:makeAutoObservable 黑盒——你不知道哪些属性是 observable
// 哪些方法是 action,哪些 getter 是 computed
makeAutoObservable(this, {}, { autoBind: true })
}

// computed:看起来像 getter,实际是缓存的响应式计算
get activeWorkspace() {
return this.workspaces.find((ws) => ws.id === uiStore.activeWorkspaceId)
}

get activeBoard() {
return this.activeWorkspace?.boards.find(
(b) => b.id === uiStore.activeBoardId
)
}

// 🔥 痛点2:computed 跨 Store 引用——循环依赖很容易出现
get filteredTasks() {
return this.activeBoard?.stages.flatMap((stage) =>
stage.tasks.filter((task) => {
if (uiStore.searchQuery && !task.title.includes(uiStore.searchQuery))
return false
return true
})
)
}

updateTaskTitle(wsId, boardId, stageId, taskId, newTitle) {
const ws = this.workspaces.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const stage = board?.stages.find((s) => s.id === stageId)
const task = stage?.tasks.find((t) => t.id === taskId)
if (task) task.title = newTitle
}

// 🔥 痛点3:异步操作必须用 flow(Generator 语法),不能用 async/await
syncTaskToServer = flow(function* (taskId) {
this.syncStatus = 'loading'
try {
yield fetch(`/api/tasks/${taskId}`, { method: 'PUT' })
this.syncStatus = 'succeeded'
} catch (err) {
this.syncStatus = 'failed'
}
})
}

class UIStore {
activeWorkspaceId = null
activeBoardId = null
sidebarCollapsed = false
searchQuery = ''

constructor() {
makeAutoObservable(this)

// 🔥 痛点4:副作用散落在各 Store 的 constructor 里
// autorun、reaction 的清理时机是个难题
reaction(
() => this.activeWorkspaceId,
(wsId) => {
// 切换工作区时自动加载看板
this.loadBoardsForWorkspace(wsId)
}
)
}

setSearchQuery(query) {
this.searchQuery = query
}

toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed
}

loadBoardsForWorkspace = flow(function* (wsId) {
// ...
})
}

// 🔥 痛点5:Store 之间需要手动连接,没有统一的组合机制
const projectStore = new ProjectStore()
const uiStore = new UIStore()

// 🔥 痛点6:React 集成需要额外的 Provider 或 Context
const StoreContext = React.createContext({ projectStore, uiStore })

组件消费

import { observer } from 'mobx-react-lite'

// 🔥 痛点7:每个组件都必须包 observer HOC,漏了就不响应式
const TaskBoard = observer(() => {
const { projectStore, uiStore } = useContext(StoreContext)

return (
<div>
<input
value={uiStore.searchQuery}
onChange={(e) => uiStore.setSearchQuery(e.target.value)}
/>
{/* 🔥 痛点8:忘记 observer 包裹子组件 === 不更新 */}
{projectStore.filteredTasks?.map((task) => (
<TaskCard key={task.id} task={task} />
))}
</div>
)
})

// 🔥 忘了 observer?恭喜你收获一个沉默的 bug,不报错,只是不更新
const TaskCard = observer(({ task }) => {
const { projectStore } = useContext(StoreContext)
return (
<div>
<h3>{task.title}</h3>
<button onClick={() => projectStore.syncTaskToServer(task.id)}>
同步
</button>
</div>
)
})

MobX 结论

API 繁琐度:⭐⭐⭐

痛点说明
黑盒行为makeAutoObservable 隐式推断 observable/action/computed
flow 语法异步操作必须用 Generator,不能直接 async/await
observer 传染每个响应式组件都必须包 observer(),漏了不报错只是失效
多 Store 耦合Store 之间互相引用容易形成循环依赖
副作用管理reaction / autorun 的创建和销毁时机是隐患

MobX-State-Tree:结构化 OOP 的类型体操

MobX-State-Tree(MST)在 MobX 之上增加了运行时类型系统状态树结构。听起来很美好,但代价是什么?

Model 定义:类型地狱

import { types, flow, getSnapshot, applySnapshot, getRoot, getParent } from 'mobx-state-tree'

// 🔥 痛点1:必须用 MST 自己的类型系统重新定义一切
// TypeScript 的类型和 MST 的 types 是两套体系,完全不互通
const SubtaskModel = types.model('Subtask', {
id: types.identifier,
text: types.string,
done: false,
})

const CommentModel = types.model('Comment', {
id: types.identifier,
userId: types.string,
content: types.string,
createdAt: types.number,
})

const TaskModel = types
.model('Task', {
id: types.identifier,
title: types.string,
description: '',
assigneeId: types.maybeNull(types.string),
priority: types.enumeration(['low', 'medium', 'high', 'urgent']),
tags: types.array(types.string),
done: false,
dueDate: types.maybeNull(types.string),
subtasks: types.array(SubtaskModel),
comments: types.array(CommentModel),
})
.actions((self) => ({
setTitle(newTitle) {
self.title = newTitle
},
complete() {
self.done = true
self.tags.push('system-completed')
self.subtasks.forEach((st) => (st.done = true))
},
// 🔥 痛点2:flow 语法依然是 Generator,和 MobX 同款痛苦
syncToServer: flow(function* () {
try {
yield fetch(`/api/tasks/${self.id}`, {
method: 'PUT',
body: JSON.stringify(getSnapshot(self)),
})
// 🔥 痛点3:跨层访问必须用 getRoot / getParent
getRoot(self).connection.setLastSync(Date.now())
} catch (err) {
console.error('同步失败')
}
}),
}))
.views((self) => ({
get isOverdue() {
return self.dueDate && new Date(self.dueDate) < new Date()
},
}))

const StageModel = types.model('Stage', {
id: types.identifier,
title: types.string,
color: '#ccc',
tasks: types.array(TaskModel),
})

const BoardModel = types.model('Board', {
id: types.identifier,
title: types.string,
stages: types.array(StageModel),
})

const WorkspaceModel = types.model('Workspace', {
id: types.identifier,
name: types.string,
members: types.array(types.string),
boards: types.array(BoardModel),
})

const UserModel = types.model('User', {
id: types.identifier,
name: types.string,
avatar: '',
online: false,
})

const ConnectionModel = types
.model('Connection', {
status: types.enumeration(['connected', 'disconnected', 'reconnecting']),
lastSyncAt: types.maybeNull(types.number),
})
.actions((self) => ({
setLastSync(time) {
self.lastSyncAt = time
},
setStatus(status) {
self.status = status
},
}))

const UIModel = types
.model('UI', {
activeWorkspaceId: types.maybeNull(types.string),
activeBoardId: types.maybeNull(types.string),
sidebarCollapsed: false,
taskDetailModal: types.optional(
types.model({ visible: false, taskId: types.maybeNull(types.string) }),
{}
),
searchQuery: '',
})
.actions((self) => ({
setActiveWorkspace(id) { self.activeWorkspaceId = id },
setActiveBoard(id) { self.activeBoardId = id },
toggleSidebar() { self.sidebarCollapsed = !self.sidebarCollapsed },
setSearchQuery(q) { self.searchQuery = q },
}))

// 🔥 痛点4:根 Store 要把所有 Model 组装起来,层级关系必须在这里声明
const RootStore = types
.model('RootStore', {
workspaces: types.array(WorkspaceModel),
users: types.map(UserModel),
ui: types.optional(UIModel, {}),
connection: types.optional(ConnectionModel, { status: 'disconnected' }),
})
.actions((self) => ({
// 🔥 痛点5:移动任务跨分支——MST 不允许同一节点存在于两处
// 必须先 detach 再 push,或者手动 clone
moveTask(wsId, boardId, fromStageId, toStageId, taskId) {
const ws = self.workspaces.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const fromStage = board?.stages.find((s) => s.id === fromStageId)
const toStage = board?.stages.find((s) => s.id === toStageId)
if (fromStage && toStage) {
const task = fromStage.tasks.find((t) => t.id === taskId)
if (task) {
// 🔥 不能直接 push(task),因为 task 已经属于 fromStage
// 必须先 detach 或 clone
const snapshot = getSnapshot(task)
fromStage.tasks.remove(task)
toStage.tasks.push(snapshot)
}
}
},
}))
.views((self) => ({
get activeBoard() {
const ws = self.workspaces.find((w) => w.id === self.ui.activeWorkspaceId)
return ws?.boards.find((b) => b.id === self.ui.activeBoardId)
},
get filteredTasks() {
const board = self.activeBoard
if (!board) return []
return board.stages.flatMap((stage) =>
stage.tasks.filter((task) => {
if (self.ui.searchQuery && !task.title.includes(self.ui.searchQuery))
return false
return true
})
)
},
}))

// 🔥 痛点6:实例化也要传完整的初始 snapshot
const rootStore = RootStore.create({
workspaces: [],
users: {},
})

组件消费

import { observer } from 'mobx-react-lite'

// 和 MobX 一样,observer 传染
const TaskBoard = observer(() => {
const store = useContext(StoreContext)

return (
<div>
<input
value={store.ui.searchQuery}
onChange={(e) => store.ui.setSearchQuery(e.target.value)}
/>
{store.filteredTasks.map((task) => (
<TaskCard key={task.id} task={task} />
))}
</div>
)
})

const TaskCard = observer(({ task }) => {
return (
<div>
<h3>{task.title}</h3>
{/* 🔥 痛点7:action 绑定在 model 实例上,看起来爽但 debug 时调用链极长 */}
<button onClick={() => task.complete()}>完成</button>
<button onClick={() => task.syncToServer()}>同步</button>
{task.isOverdue && <span style={{ color: 'red' }}>已逾期</span>}
</div>
)
})

MST 结论

API 繁琐度:⭐⭐⭐⭐⭐(满星)

痛点说明
双重类型系统TypeScript 类型和 MST types 完全独立,等于写两遍
Model 爆炸每个实体都要定义 Model + Actions + Views,比 Redux 还多
树结构限制同一节点不能属于两个父节点,移动操作必须 detach/clone
flow 语法和 MobX 一样,异步只能用 Generator
学习曲线getRoot / getParent / getSnapshot / applySnapshot 各种 API
运行时开销每个节点都被运行时类型系统包装,大型状态树有性能隐患

Pinia:Vue 生态的对照组——React 开发者看了会沉默

虽然 Pinia 属于 Vue 生态,但把它放在这里是为了形成一个残酷的对比——同样的需求,Vue 的状态管理可以简洁到什么程度。

Store 定义

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// ✅ Composition API 风格——和写组件一模一样
export const useProjectStore = defineStore('project', () => {
// 状态就是 ref
const workspaces = ref([])
const users = ref({})
const connection = ref({ status: 'disconnected', lastSyncAt: null })

// 派生状态就是 computed——内置缓存,不需要 reselect
const activeWorkspace = computed(() =>
workspaces.value.find((ws) => ws.id === useUIStore().activeWorkspaceId)
)

const activeBoard = computed(() =>
activeWorkspace.value?.boards.find(
(b) => b.id === useUIStore().activeBoardId
)
)

// ✅ 直接 async/await,不需要 createAsyncThunk、flow、或任何包装
async function syncTaskToServer(taskId) {
try {
await fetch(`/api/tasks/${taskId}`, { method: 'PUT' })
connection.value.lastSyncAt = Date.now()
} catch (err) {
console.error('同步失败')
}
}

// ✅ 深层更新?Vue 的响应式系统天然支持,不需要 immer
function updateTaskTitle(wsId, boardId, stageId, taskId, newTitle) {
const ws = workspaces.value.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const stage = board?.stages.find((s) => s.id === stageId)
const task = stage?.tasks.find((t) => t.id === taskId)
if (task) task.title = newTitle // ✅ 直接赋值,响应式自动追踪
}

function completeTask(wsId, boardId, stageId, taskId) {
const ws = workspaces.value.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const stage = board?.stages.find((s) => s.id === stageId)
const task = stage?.tasks.find((t) => t.id === taskId)
if (task) {
task.done = true
task.tags.push('system-completed')
task.subtasks.forEach((st) => (st.done = true))
}
}

function moveTask(wsId, boardId, fromStageId, toStageId, taskId) {
const ws = workspaces.value.find((w) => w.id === wsId)
const board = ws?.boards.find((b) => b.id === boardId)
const fromStage = board?.stages.find((s) => s.id === fromStageId)
const toStage = board?.stages.find((s) => s.id === toStageId)
if (fromStage && toStage) {
const idx = fromStage.tasks.findIndex((t) => t.id === taskId)
if (idx > -1) {
const [task] = fromStage.tasks.splice(idx, 1)
toStage.tasks.push(task)
}
}
}

return {
workspaces,
users,
connection,
activeWorkspace,
activeBoard,
updateTaskTitle,
completeTask,
moveTask,
syncTaskToServer,
}
})

// UI Store——独立定义,互相引用毫无压力
export const useUIStore = defineStore('ui', () => {
const activeWorkspaceId = ref(null)
const activeBoardId = ref(null)
const sidebarCollapsed = ref(false)
const searchQuery = ref('')
const filters = ref({ priority: [], assignee: [], tags: [] })

function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
}

return {
activeWorkspaceId,
activeBoardId,
sidebarCollapsed,
searchQuery,
filters,
toggleSidebar,
}
}, {
// ✅ 持久化?一行搞定
persist: true,
})

组件消费

<script setup>
import { storeToRefs } from 'pinia'

const projectStore = useProjectStore()
const uiStore = useUIStore()

// ✅ storeToRefs 保持响应式解构,不丢失更新
const { searchQuery, filters } = storeToRefs(uiStore)

// ✅ computed 自动追踪依赖,不需要 selector、不需要 shallow
const filteredTasks = computed(() => {
return projectStore.activeBoard?.stages.flatMap((stage) =>
stage.tasks.filter((task) => {
if (searchQuery.value && !task.title.includes(searchQuery.value))
return false
if (
filters.value.priority.length &&
!filters.value.priority.includes(task.priority)
)
return false
return true
})
)
})
</script>

<template>
<div>
<input v-model="searchQuery" placeholder="搜索任务..." />
<!-- ✅ 不需要 observer、不需要 memo、不需要 selector -->
<TaskCard
v-for="task in filteredTasks"
:key="task.id"
:task="task"
@complete="projectStore.completeTask(/*...*/)"
/>
</div>
</template>

Pinia 结论

API 繁琐度:⭐(几乎没有额外心智负担)

对比维度React 状态库们Pinia
定义 Store各种奇特的 API(createSlice / atom / proxy / makeAutoObservable)defineStore + 标准 Composition API
深层更新Spread 地狱 / Immer / Proxy 各显神通直接赋值,响应式自动追踪
派生状态useMemo / createSelector / derived atom / computed gettercomputed,内置缓存,天然响应式
异步操作createAsyncThunk / flow / writable atom直接 async/await
性能优化selector + shallow / observer / useSnapshot不需要手动优化,Vue 自动细粒度追踪
持久化中间件 / atomWithStorage / 手动 subscribe{ persist: true }
DevTools需要额外配置内置,零配置

React 开发者看到这里应该沉默了。 不是 Pinia 特别神奇,而是 Vue 的响应式系统从底层解决了 React 的 "不可变数据 + 手动优化渲染" 带来的一系列衍生问题。


深度对比:我们到底在痛苦什么?

1. API 设计:谁都不完美

维度ZustandReduxJotaiValtioMobXMSTPinia
学习曲线极高
深层更新Spread 地狱Immer 缓解Spread 地狱直接修改 ✅直接修改 ✅直接修改 ✅直接修改 ✅
类型推导中间件会丢失较好较好Proxy 类型弱装饰器类型差双重类型系统完美
样板代码量极高极高极低

2. 性能优化的"税"

Zustand : useStore(selector, shallow) ← 每个字段手写
Redux : useSelector + createSelector ← 每个派生数据手写
Jotai : 天然细粒度,但 atom 太多管不过来
Valtio : useSnapshot 自动追踪,但 memo 会失效
MobX : observer 自动追踪,但忘记包裹就是 bug
MST : 和 MobX 一样,加上运行时类型开销
Pinia : 不需要手动优化,Vue 自动追踪 ✅

结论:没有银弹。 每个库都要求开发者以不同的方式"交税"来换取性能。

3. 中间件与副作用

需求ZustandReduxJotaiValtioMobXMSTPinia
持久化persist 中间件redux-persistatomWithStorage手动 subscribe手动 autorunonSnapshot{ persist: true }
DevToolsdevtools 中间件内置jotai-devtoolsdevtools 工具mobx-devtools内置内置,零配置
异步处理直接 asynccreateAsyncThunkasync atom直接 asyncflow (Generator)flow (Generator)直接 async
日志中间件redux-logger无标准方案subscribespyonActionDevTools 内置
中间件组合嵌套括号middleware 数组无概念无概念无概念无概念插件系统

总结:状态库之"殇"

状态库的繁琐,本质上是 "业务逻辑的复杂性"与"框架抽象的有限性" 之间不可调和的冲突。

我们真正痛苦的是什么?

  1. 深层嵌套是原罪:无论哪个库,5 层嵌套的 .find().map() 都是噩梦。解法不在库,在数据结构——扁平化(Normalize)你的 State
  2. 样板代码是智商税:Redux 的 Action/Reducer/Selector 三件套,Jotai 的 Atom 碎片,Zustand 的 Selector+Shallow——没有一个库能让你"只关心业务"。
  3. 隐式行为是定时炸弹:Valtio 的 snap vs state、MobX 的 observer 包裹——开发体验的"简单"总是以调试成本为代价。

最终建议

场景推荐理由
简单应用useState + useContext不需要状态库
中等复杂度ZustandAPI 简单,社区活跃
需要极致性能Jotai原子化天然避免不必要渲染
团队有 OOP 背景MobX但要严格约束 Store 边界
大型企业应用Redux Toolkit规范化有利于协作,但要忍受模板代码
需要运行时校验MobX-State-Tree适合数据结构严格的领域模型,但学习成本极高
原型快速开发ValtioAPI 最少,但注意 snap 陷阱
Vue 技术栈Pinia几乎没有学习成本,状态管理的理想形态

最终你会发现,状态管理的极致,不是发明更好的库,而是减少状态的存在。

你能删掉的状态,永远比你能管理好的状态更优雅。


附录:流行度看板 (2026 Q1)

在感性讨论之外,我们来看看真正的理性数据——谁在支撑着目前的前端工业界?

状态库 (NPM 2026 估算)周下载量简评
Redux (RTK)~8.85M工业界的绝对基石,存量巨大,规范严谨
Zustand~4.20MReact 新建项目的“默认选项”,增长极其迅猛
Pinia~2.90MVue 生态唯一的官方“真名天子” (对照组)
MobX~1.25M响应式/OOP 派系的常青树,大型复杂表单首选
Jotai~920K原子化方案的佼佼者,深受模块化架构推崇
Valtio~480K追求开发体验极致的 Proxy 方案,黑马姿态
MobX-State-Tree~360K适合追求强类型、结构化状态树的垂直细分领域

注:数据来源于 2026 年初 NPM 官方公开统计及其增长趋势预测。