跳到主要内容

3 篇博文 含有标签「react」

查看所有标签

React Compiler

· 阅读需 6 分钟
Wuji
AI Engineer

React Compiler 是 React 团队推出的一款实验性编译器,旨在通过自动化手段彻底解决 React 应用中的性能优化难题。

是什么

React Compiler(曾用名 React Forget)是一个**构建时(Build-time)**工具。它通过静态分析你的 React 代码,自动为组件和 Hook 添加 memoization(内存化/缓存)。

核心目标

  • 消除手动优化:不再需要手动编写 useMemouseCallbackReact.memo
  • 保持“响应式”:确保 UI 只在相关数据真正变化时才重新渲染。
  • 心智负担降级:让开发者专注于业务逻辑,而不是如何规避不必要的渲染。

怎么用

目前 React Compiler 已经随 React 19 进入测试阶段,你可以通过以下步骤将其集成到项目中。

1. 技术栈要求

  • React 19:原生支持,无需额外配置。
  • React 17 / 18:最低支持到 17.0.0。但需要额外安装 react-compiler-runtime 包作为依赖,以提供旧版本的运行时支持。
  • 使用了 Babel 或支持 Babel 插件的项目(如 Vite、Next.js)。

2. 安装编译器插件

pnpm add -D babel-plugin-react-compiler eslint-plugin-react-compiler

# 如果 React 版本低于 19,还需安装运行时适配包
pnpm add react-compiler-runtime

3. 环境检测

在正式使用前,建议运行健康检查工具来评估项目的兼容性:

npx react-compiler-healthcheck@latest

4. 项目配置 (以 Vite 为例)

修改 vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['babel-plugin-react-compiler', { target: '19' }],
],
},
}),
],
})

注意点

虽然编译器非常智能,但它依赖于某些假设,因此你需要遵循以下原则:

1. 严格遵守 React 规则 (Rules of React)

编译器假定你的代码是“合法”的 React 代码。以下行为会导致优化失效甚至出错:

  • 不要在循环或条件语句中调用 Hook
  • 不要直接修改 Props 或 State(保持数据不可变性)。
  • 不要在渲染函数中产生副作用(如修改全局变量)。

2. 逃生舱:"use no memo"

如果你发现某个特定的组件因为特殊原因不适合被自动优化,可以在文件顶部或函数顶部添加指令:

function MySpecialComponent() {
'use no memo'
// 此时编译器将跳过该组件
}

3. 配合 ESLint 插件

强烈建议安装 eslint-plugin-react-compiler。它会在开发阶段实时提醒你哪些代码违反了编译器的优化规则。


原理

React Compiler 的工作原理类似于一个高级的 JavaScript 引擎优化器,但它专门针对 React 的语义进行了定制。

工作流 (Pipeline)

  1. AST 解析:将源代码解析为抽象语法树。
  2. HIR 转换:将 AST 转换为 HIR (High-level Intermediate Representation)。HIR 比 AST 更清晰地表达了代码的控制流(循环、分支)和数据流。
  3. SSA 分析:使用 SSA (Static Single Assignment) 静态单赋值形式进行分析,追踪每一个变量的定义和使用周期。
  4. Reactive Scopes 识别:这是核心步骤。编译器会自动识别哪些代码块构成了“响应式作用域”,并根据输入(Props/State)计算依赖项。
  5. 代码生成:根据分析结果,在最终生成的 JS 代码中注入缓存逻辑(类似自动生成的 useMemo)。

结果示例 (编译前后对比)

为了更好地理解,我们来看一个简单的组件在编译前后的差异。

编译前 (原始代码):

function FriendList({ friends }) {
const onlineCount = useFriendOnlineCount();
return (
<div className="friend-list">
<h1>Online: {onlineCount}</h1>
{friends.map(f => <Friend key={f.id} friend={f} />)}
</div>
);
}

编译后 (等效逻辑): 编译器会生成类似下方的代码,使用内部的缓存机制(通常称为 _cuseMemoCache)来存储结果:

function FriendList(t0) {
const $ = _c(4); // 初始化 4 个缓存槽位
const { friends } = t0;
const onlineCount = useFriendOnlineCount();

// 1. 自动缓存渲染出的 h1 标题
let t1;
if ($[0] !== onlineCount) {
t1 = <h1>Online: {onlineCount}</h1>;
$[0] = onlineCount;
$[1] = t1;
} else {
t1 = $[1];
}

// 2. 自动缓存整个列表及外层 div
let t2;
if ($[2] !== friends || $[0] !== onlineCount) {
t2 = (
<div className="friend-list">
{t1}
{friends.map(f => <Friend key={f.id} friend={f} />)}
</div>
);
$[2] = friends;
$[3] = t2;
} else {
t2 = $[3];
}

return t2;
}

为什么它比我们手写的更强?

  • 细粒度缓存:它可以为每一行表达式单独做缓存,而我们手写 useMemo 通常只能粗粒度地包裹整个对象或函数。
  • 语义理解:它理解 React 的核心语义(如不可变性),能够比通用编译器做出更大胆且安全的假设。

额外的思考

React 一直将纯粹的 JS 作为卖点,增加 Compiler 后,我们写的组件与真正运行的组件可能会有很大的差异。这是否意味着 React 正在离其“纯粹 JavaScript”的初衷越来越远?

其实恰恰相反。React Compiler 的出现,是为了让 React 的开发体验回归到更纯粹的 JavaScript。

在没有 Compiler 的时代,为了追求性能,我们不得不违背直觉地在代码中大量填充 useMemouseCallbackReact.memo。这些“性能补丁”不仅增加了代码的冗余,更破坏了 JavaScript 逻辑的连贯性——开发者在写每一行数据处理逻辑时,脑子里都得绷着一根弦:“这个引用是否会变?”、“我是不是漏掉了依赖项?”。

这种负担,本质上是对 React “手动挡”性能优化的妥协。

有了 Compiler 后,虽然编译后的产物变得复杂了(就像 V8 引擎会对你的 JS 进行大量 JIT 优化一样),但你手写的代码却变简单了。你可以重新像写普通 JavaScript 一样去写 React:

  • 不再纠结缓存:不需要再到处包裹 useMemo
  • 不再心累引用:不需要再为了防止子组件重渲染而强行 useCallback
  • 返璞归真:你可以把精力完全放在业务逻辑的表达上。

这标志着 React 从“开发者负责优化”转向了“框架负责优化”。

正如我们现在不会去关心 Babel 是如何把 ES6 转换成 ES5 的,未来我们也无需关心 React Compiler 是如何做缓存的。它将这种机械、重复且易出错的优化工作从人类大脑移交给了算法,让我们能把 100% 的精力放回业务逻辑本身。

React 依然是那个 JavaScript 框架,只是它变得更加聪明,让你不再需要为了性能而写出“非人”的代码。通过这种“魔法”,React 实际上完成了一次向 JavaScript 原始心智模型的回归。

状态库之殇

· 阅读需 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 官方公开统计及其增长趋势预测。

What can I say?

· 阅读需 16 分钟
Mike

缺陷

react 是个好框架吗?无疑是的 但在我看来他有一些问题。 作为一个工具,他设置了太多限制,让使用者顾虑太多。 作为同行,同样是完成页面渲染的任务,vue、solidjs 等,不太需要使用者注意太多,定义好视图,注册好事件,在定义下状态,使用固定的生命周期规范即可,页面就如你所愿了; 但对 react 来说,没那么简单:

  • 你要按照我的规定定义状态
  • 我们是纯函数,生命周期是什么?
  • 你想读状态?先看文档吧
  • 你想改状态?不好意思,我不记得你改了两次
  • 你想在渲染时改状态?不好意思,有副作用
  • 等等

综上,如果是一个新接触的 react 使用者,一定会被这些奇怪的规范坑,当查文档、问 AI 后恍然大悟:原来都是 react 的设计在坑我 所以说,作为辛苦开发挣点血汗钱的小前端来说,居然在用一个带刺的犁耕地,时不时被扎一下,真是不爽 这一点来说 react 的设计无疑是费力的

根源: 技术大拿的自嗨 不知大家有没有这个经历,在我们刚刚学习前端的时候,碰到过自诩清高的人说 react 更好,其他框架是智力低的人学的 在讨论react 相关技术的博客上,“心智模型”、“代数效应”、“副作用”等词汇频繁出现,甚至是这辈子第一次听说 更明显的,在提出 react 缺点、框架比较的讨论上,一定有人站在 react 这一边人身攻击贬低别人 以上就说明一个问题,鄙视链 已经存在,而且拥护 react 的人已经站在了顶端,一个神已经诞生了,神是没有缺点的,只有人理解不了神 一旦你发现了什么,就是你水平低,你自己的问题

例子

我们先从最常用的举例

setState

在 函数组件时代,只能用 setState定义组建状态 但这个 setState 真是让人费解 setState 是异步的,set 后我想读取一下,抱歉,我还没来得及更新 react 说:这是「调度」「可预测性」「避免半成品 UI」 但我寻思我一个写代码的,刚 set 的再 get 怎么就不行了?这不是欺负老实人吗

你得 自己存储新的 state,使用时自己传过去,要么就等到下一周期

useEffect

这个 API 可谓是 react 的灵魂了 官方的定义是每次 render 后根据 deps 判断是否执行 也就是说他并非简单的生命周期的含义,怕我们不懂甚至还有专门的课程告诉我们什么时候不该用 但我想说,所谓的pure、hook 等概念让这个 api 成为了问题发生器 规范告诉我们用到的状态都要加到 deps 里,否则依赖收集不到下次 render 会有闭包陷阱 但需求告诉我我不能把用过的 state 加到 deps 里,因为一旦我加了,这个细枝末节的依赖会导致我的清理回调过早执行

肯定是我用的不对 记住这个典型场景的定义:我需要用到这个值,但不希望他的变化触发 effect 我们可以想到另一个 api:useRef useRef 突破了render 的限制,就像一个函数外的全局变量,恰好能满足需求

const useLastRef = (value) => {
const ref = useRef(value);
// 为什么有这个 effect,还是经典原因,react 不允许在 render 阶段进行有副作用的更新,即 render 阶段不要给任何 react 相关变量赋值,有副作用的操作都要在 useEffect 里进行且要加上依赖
useEffect(() => {
ref.current = value;
}, [value]);
return ref;
}

const useLastCallback = (value) => {
const ref = useLastRef(value);
return useCallback((...args) => ref.current(...args), [ref]);
}

function Comp({date}) => {
const lastDate = useLastRef(date);
// 例如 我们只是要在这个组件的初始化时同时初始化一个连接,假如父组件传入一个每秒更新的 date,那么显然不可能把 date 加入依赖数组导致每一秒初始化一下连接
useEffect(() => {
connect.then((res) => message.success('connect success date is ' + lastDate.current'))
return () => {
disconnect();
}
}, []);
}

在 react 19 后,新增了一个 API:useEffectEvent

还有就是官方告诉我们,不是任何关于状态的操作都要使用 useEffect,反而推荐能不用就不用,用户和事件触发的都放到事件处理函数里,即在组件内定义的普通函数,只有组件自发和一些内部的数据传递才不得不需要用 useEffect

useSyncExternalStore

useSyncExternalStore(简称 uSES)是 React 18 引入的一个非常底层、甚至带点“补救”色彩的 Hook。

如果说 Fiber 和 Concurrent Mode(并发模式)是 React 引以为傲的超跑引擎,那么 useSyncExternalStore 就是为了防止这辆超跑把第三方状态库(Zustand, Redux, Valtio 等)甩出车道而专门设计的**“安全锁”**。

要彻底弄懂它,我们必须先理解它试图解决的那个极其恐怖的并发灾难:UI 撕裂(Tearing)

1. 灾难前传:为什么传统的 useEffect 订阅失效了?

在 React 18 之前,我们如果在 React 里订阅一个外部数据(比如 window.innerWidth 或者 Redux Store),通常是这么写的:

// ❌ React 18 并发模式下的反模式
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);

useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);

return width;
}

并发模式下的灾难推演: 假设你有一个包含 1000 个子组件的超长列表,每个子组件都在读取这个 width

  1. 渲染开始: React 开始并发渲染,处理了前 500 个组件。此时 width 是 1000px。
  2. 被迫让出: 时间切片(5ms)用完,React 暂停渲染,把主线程还给浏览器。
  3. 外部突变: 就在这停顿的几毫秒里,用户猛地拖拽了浏览器窗口,width 变成了 800px!外部 Store(window 对象)已经变了。
  4. 恢复渲染: React 醒来,继续渲染剩下的 500 个组件。但此时它读取到的 width 变成了 800px。
  5. UI 撕裂发生: 屏幕上同时出现了 1000px 样式的上半部分,和 800px 样式的下半部分。同一个状态在同一次 Render 中出现了两个不同的值。 这对 UI 框架来说是致命的。

2. 救场英雄:useSyncExternalStore 的 API 设计

为了解决这个问题,React 团队推出了这个名字极长的 Hook。它要求状态库作者必须把控制权交还给 React。

它的 API 只有三个参数:

const state = useSyncExternalStore(
subscribe, // 1. 订阅函数:告诉 React 数据变了怎么通知它
getSnapshot, // 2. 快照函数:告诉 React 怎么获取当前最新的、不可变的值
getServerSnapshot // 3. (可选) SSR 时的初始值
);

还是拿 window.innerWidth 举例,用 uSES 重写后是这样的:

// ✅ 现代 React 订阅外部状态的绝对标准
function subscribe(callback) {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
}

function getSnapshot() {
return window.innerWidth;
}

function useWindowWidth() {
return useSyncExternalStore(subscribe, getSnapshot);
}

3. 底层魔法:它是如何防止撕裂的?

既然它叫 useSyncExternalStore,这里的 Sync (同步) 就是整个魔法的核心。它不仅是一个订阅工具,更是一个并发打断器

当你在并发渲染中使用它时,React 底层会做非常严苛的检查:

  1. React 开始渲染,读取了 getSnapshot() 的值。
  2. 渲染过程中,组件树被暂停,主线程让出。
  3. 就在这时,外部数据变了(触发了 subscribe 绑定的 callback)。
  4. React 醒来准备继续渲染,但在继续之前,它会偷偷再调用一次 getSnapshot()
  5. 发现不一致! React 发现当前拿到的快照和渲染上半场拿到的快照不一样。
  6. 极其暴力的“Sync”干预: React 会立刻判定:“这次并发渲染已经被污染了,草稿作废!” 接着,React 会放弃并发模式,强制切换为同步(Synchronous)模式,锁死主线程,用最新的快照值,从头到尾一口气把整个组件树重新渲染一遍。

4. 为什么状态管理库(Zustand/Valtio/Redux)全靠它续命?

在 React 18 刚发布时,几乎所有的第三方状态库都炸锅了,因为它们自己维护的 Store 都是“外部(External)”的,天然不受 React 并发调度的管控。

  • Zustand: 它的核心依然是一个原生的 JS 闭包变量。当你调用 useStore 时,底层其实就是调用了 useSyncExternalStore(store.subscribe, store.getState)
  • Valtio: Valtio 的 Proxy 负责突变,克隆出冻结的快照交给 getSnapshot,然后通过 uSES 把这个快照极其安全地注入到 React 的渲染流中。
  • Redux Toolkit: useSelector 底层同样全面重构,接入了 uSES。

总结

useSyncExternalStore 是 React 官方对并发模式副作用的一次“妥协与修补”。

React 承认:“我管不了外面的世界(DOM、第三方状态库)。如果外面的世界变了,为了防止我的 UI 撕裂,我只能放弃并发的优雅,退化回暴力的同步渲染来保证正确性。”

作为普通开发者,你基本不需要手写这个 Hook。但理解它,你就彻底懂了现代 React 状态库的底层架构,以及 React 在面对极其复杂的并发调度时,那道用来保底的最后防线。

并发模式的代价

React 的 Concurrent Mode(并发模式)可以说是前端工程史上最疯狂的架构实验之一。它试图在单线程的 JavaScript 环境里,用应用层的代码去模拟操作系统的“线程调度”。

这种“逆天而行”的架构虽然带来了极致的理论响应速度,但也彻底打开了潘多拉魔盒,把极其复杂的“并发锁”、“内存逃逸”等概念强加给了普通的前端开发者。

除了前面提到的 UI 撕裂 (Tearing)防撕裂 Hook (useSyncExternalStore) 之外,并发模式还带来了以下几个极其折磨人的底层问题:

1. useRef 的突变污染陷阱 (The Mutation Trap)

在非并发时代,只要你在 Render 函数里同步修改了 useRef,虽然官方不推荐,但通常“跑起来没问题”,因为渲染是一气呵成的。但在并发模式下,这是致命的

灾难推演: 假设你在 Render 阶段写了 ref.current = ref.current + 1 来记录组件渲染的次数。

  1. React 开始低优先级渲染组件 A,执行到了这一行,ref.current 变成了 2。
  2. 突然,高优先级的用户点击事件进来了,React 决定打断并废弃这次组件 A 的渲染。
  3. 陷阱来了: 渲染虽然被废弃了,但 ref.current 是一个脱离了 React 状态快照的普通 JS 引用!它的突变是不可逆的。它已经变成了 2。
  4. 当 React 再次重新渲染组件 A 时,ref.current 又加了 1,变成了 3。最终页面虽然只渲染成功了一次,但 Ref 的值却莫名其妙变成了 3,导致与外部系统(如第三方图表、视频播放器实例)彻底脱节。

2. 开发环境的“双重调用”噩梦 (Strict Mode Double Invocation)

如果你升级到 React 18,你会发现一个让人血压飙升的现象:在开发环境下,所有的 useEffect 都会莫名其妙地执行两次! (挂载 -> 卸载 -> 再次挂载)。

  • 开发者的痛苦: 如果你的 Effect 里是发请求、埋点,你会看到 Network 面板里发了两次请求。很多人为了解决这个问题,搞出了各种奇技淫巧(比如用 useRef 记录是否请求过),导致代码越来越丑。
  • React 团队的苦衷: 这是他们故意的。并发模式下,React 未来会引入 Offscreen API(类似 Vue 的 Keep-Alive)。React 可能会在后台预渲染组件(挂载),发现用户没切过来,就暂停它(卸载),等切过来再恢复(再次挂载)。React 团队为了强迫开发者写出完美的、支持随时中断和恢复的清理函数(Cleanup Function),干脆在开发环境模拟了这种极端情况。
  • 结论: 框架为了未来架构的“政治正确”,把压力和不适感直接转嫁给了当下的开发者。

3. 被废弃渲染带来的 GC (垃圾回收) 惩罚

并发模式最大的卖点是:遇到高优先级任务,直接丢弃低优先级任务。

  • 隐形代价: 被丢弃的低优先级任务并不是真的“没发生过”。CPU 已经辛辛苦苦计算了那棵庞大的 Fiber 树(workInProgress tree),并且分配了大量的内存。
  • GC 风暴: 当它被无情抛弃时,这棵庞大的内存树变成了无主之地。JavaScript 引擎的垃圾回收器(GC)必须花费额外的算力去清理这些垃圾。
  • 结论: 如果你的应用充满了大量复杂的低优先级运算和高频的用户输入,并发模式不仅没有让你变快,反而可能因为频繁的打断和疯狂的 GC 导致浏览器卡顿。也就是所谓的“为了不卡顿而引入了新的卡顿”。

4. useMemo 失去“记忆”的语义不确定性

在以前,很多开发者把 useMemo 当作一个绝对可靠的缓存:只要依赖不变,它的值和内存地址就绝对不变。

  • 并发模式下的反转: React 官方在 18 的文档里明确警告:useMemo 只是一个性能优化的“提示(Hint)”,而不是语义上的“保证(Guarantee)”。
  • 为什么? 因为在并发模式下,如果内存吃紧,或者某次渲染被打断,React 内部完全有权利擅自清空 useMemo 的缓存,并在下次强行重新计算。
  • 引发的 Bug: 如果你把 useMemo 的结果作为 useEffect 的依赖项,一旦 React 在底层擅自清空了缓存导致重新计算出新对象(引用地址改变),你的 useEffect 就会在意料之外被意外触发,引发连锁反应。

5. Suspense 的“瀑布流与请求竞态”放大效应

并发模式重度依赖 <Suspense> 来处理异步。但如果你不使用 Next.js 等元框架帮你做服务端预取(Prefetching),而是在客户端通过 Suspense 加载数据,并发打断会让请求变得不可控。

  • 当组件准备渲染,触发了抛出 Promise 的请求逻辑。
  • 突然因为高优先级更新,渲染被打断,组件树被抛弃。
  • 此时你的网络请求还在飞(浪费带宽),但它的结果已经没人要了。当下一次渲染重新启动时,如果没有极其严密的缓存层(如 React Query),它可能会再次发起一个完全相同的请求

过度设计

对于所有的前端框架或者库来说,要做的事情就一件:把用户写的 js 渲染成页面上的 html-css-js 为了完成这个任务,vue 使用了模版语法定义视图,组件化完成了组件实例和渲染,vdom 完成了 html 转化和组件更新,响应式数据完成了数据驱动页面 solidjs 使用了 JSX 定义视图,组件化完成了逻辑的初次挂载(组件函数仅运行一次),预编译机制完成了真实 DOM 的生成与精准绑定(无 VDOM),**细粒度响应式(Signals)**完成了数据到具体 DOM 节点的直接驱动页面。 angular 使用了带有结构化指令的模版语法定义视图,基于类的面向对象与装饰器完成了组件实例和渲染,**增量 DOM(Ivy 渲染引擎)**完成了 html 转化和组件更新,全局脏检查机制(Zone.js)及现代 Signals 完成了数据驱动页面。 svelte 使用了增强型 HTML 模版语法定义视图,组件化在编译阶段转化为了高度优化的独立模块,AOT(运行前)编译机制直接生成了操作真实 DOM 的原生代码(无 VDOM),**编译器劫持赋值语句(或现代 Runes)**完成了零运行时开销的数据驱动页面。 在学习这几个框架的是现时我能感觉到他们的设计是渐进的,是顺其自然的,想组件化就用类或封装结构来包装一个前端意义上的组件,想响应式就用响应式数据,都是单纯的把数据和调用关联起来

而 react 则是一个宏大到无以复加的存在,并发模式下无时无刻的 workLoop,组件化却没有为组件封装,反而搞了一套 fiber 来代替组件,hook 更是披着“纯函数”的外衣,底层却死板地依赖着极其脆弱的执行顺序链表。它将原本该由框架底层去解决的“响应式依赖收集”彻底甩锅,强迫开发者在无处不在的闭包陷阱中如履薄冰,用繁琐的依赖数组和满篇的 useMemo/useCallback 去手动修补这套宏大调度系统所带来的性能与逻辑窟窿。

生态问题

生态完整、占有率高一直是 react 的骄傲,但我想说,这个生态里有很多缺陷

  • router keep alive
  • 状态管理大乱斗
  • 表单处理的繁琐与割裂
  • 异步请求与 useEffect 的陷阱
    • 官方极其强烈地推荐你放弃手写 useEffect 发请求,直接拥抱 SWR 或 React Query 这样的库
  • CSS 方案的撕裂
  • 官方好像更喜欢Next.js

总结

推崇者们说:React 的骄傲在于“你想怎么做都行”

但我们对着需求文档才发现:“你必须自己决定怎么做”

开发到一半又发现:”不得不这么做“

额外的

应该有很多人听说过 preact

这个项目太幽默了,把 react 改造成了 vue/solid

没有 hook、也没有了 view=f(props) 的 react 哲学

性能有了质的提升,更新变成了 dom 节点级,不需要开发者管理依赖,这完全就是解决了 react 最大的两个缺陷

但 大家似乎不太喜欢他,因为这样一来 react 就不是 react 了,完全被夺舍了,如果用 preact,不如用 vue/solid,毕竟 react 缺点很多但还是前端家族中最闪耀的孩子