前端状态管理怎么选:Zustand、Jotai 与 Redux Toolkit

在 React 应用里,状态管理几乎是所有中大型项目的第一道分水岭。组件树一旦变深,props 层层透传就会让人崩溃;而随便选一个全局状态库,又可能在半年后陷入模板代码与性能陷阱。本文聚焦当前最主流的三款 React 状态方案——Zustand、Jotai 与 Redux Toolkit,从设计哲学、代码写法和选型决策三个维度拆解,帮你结合项目体量做出清醒选择。

三款库的设计哲学

Zustand:极简的 hooks 风格

Zustand 的核心思想是把”store”做成一个返回状态的 hook。它没有 Provider 嵌套,也没有 action 类型常量,你直接在一个函数里定义状态和修改函数,组件用 hook 订阅自己关心的字段即可。这种”少即是多”的取向,让它成为从中小项目到大型应用都能平滑使用的默认选择。

Jotai:自底向上的原子模型

Jotai 受 Recoil 启发,提出 atom(原子)概念:每个状态都是一个最小单元,组件只订阅自己用到的 atom。当某个 atom 变化,只有依赖它的组件重渲染,天然避免不必要的刷新。它特别适合状态高度分散、彼此耦合少的场景。

Redux Toolkit:可预测的状态容器

Redux Toolkit(RTK)是 Redux 官方推荐的现代写法,用 createSlice 把 action、reducer、初始状态收敛到一个函数里,并用 Immer 让你”直接改”状态。它的强项在于可预测性、时间旅行调试,以及 RTK Query 对服务端数据的统一管理——大型团队协作时价值明显。

同一个计数器,三种写法

用同一个需求对比代码最直观:维护一个计数器和它的自增方法。

// Zustand:一个 create 搞定状态与修改函数
import { create } from 'zustand'

const useCounter = create((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
}))

function Counter() {
  const count = useCounter((s) => s.count)
  const inc = useCounter((s) => s.inc)
  return <button onClick={inc}>{count}</button>
}
// Jotai:状态拆成最小 atom,组件只订阅用到的
import { atom, useAtom } from 'jotai'

const countAtom = atom(0)
const incAtom = atom(null, (get, set) => set(countAtom, get(countAtom) + 1))

function Counter() {
  const [count, ] = useAtom(countAtom)
  const [, inc] = useAtom(incAtom)
  return <button onClick={inc}>{count}</button>
}
// Redux Toolkit:slice 收敛 action/reducer,Immer 直接改
import { createSlice, configureStore } from '@reduxjs/toolkit'
import { useSelector, useDispatch } from 'react-redux'

const slice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: {
    inc: (state) => { state.count += 1 },
  },
})
const store = configureStore({ reducer: { counter: slice.reducer } })
const { inc } = slice.actions

function Counter() {
  const count = useSelector((s) => s.counter.count)
  const dispatch = useDispatch()
  return <button onClick={() => dispatch(inc())}>{count}</button>
}

异步数据怎么管

真实项目里状态多半来自接口,三款库的处理方式各异:

  • Zustand 直接在 store 里写 async 函数,配合 set 更新即可,轻量直观。
  • Jotai 用 atom 的异步取值或 jotai/query 扩展做数据请求。
  • Redux Toolkit 提供 RTK Query,声明式地描述接口,自动处理缓存、轮询、失效,适合接口多的中后台。
// RTK Query:声明接口,自动管缓存与失效
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (b) => ({
    getUser: b.query((id) => ({ url: `/users/${id}` })),
  }),
})
// 组件内:const { data, isLoading } = useGetUserQuery(1)

重渲染与选择器:性能关键点

全局状态最容易被忽视的成本是”不必要的重渲染”。三款库都依赖选择器(selector)来精确订阅:Zustand 靠传给 hook 的 selector 函数,Jotai 天然按 atom 粒度,RTK 则用 useSelector 配合比较函数。关于 React 重渲染与 Hooks 优化的更多细节,可参考React 性能优化实战

// Zustand:只订阅 count,其他字段变化不会触发本组件刷新
const count = useCounter((s) => s.count)

// 用 shallow 比较对象/数组,避免引用变化导致误刷新
import { shallow } from 'zustand/shallow'
const { a, b } = useCounter((s) => ({ a: s.a, b: s.b }), shallow)

选型决策表

维度ZustandJotaiRedux Toolkit
上手成本极低,几分钟可用低,需理解 atom 思维中等,概念较多
包体积(压缩)约 1KB约 5KB约 10KB,RTK Query 更大
重渲染控制选择器细粒度原子级自动useSelector + shallowEqual
服务端数据自行封装 asyncjotai/query 扩展RTK Query 开箱即用
调试与生态中等中等DevTools 时间旅行、生态最全
适合规模小到大型皆可中大型、状态分散中大型团队、接口多

常见坑与避坑清单

现象解法
Zustand 全量订阅顶层 useStore() 不传 selector,任意字段变都重渲染用选择器只取所需字段
Jotai atom 放组件内每次渲染新建 atom,状态被重置把 atom 提到模块作用域
RTK 忘记注册 reducer组件 dispatch 无反应单一 store + combineReducers 注册
服务端数据塞 store 不失效缓存陈旧、重复请求用 RTK Query / React Query / SWR

什么时候其实不需要它们

如果状态只活在一个组件内,用 useState / useReducer 就够了;如果只是为了读取接口数据,React Query / SWR 比把数据塞进全局 store 更专业。引入全局状态库的前提是:多个不相邻的组件需要共享同一份状态。包体积上,前端构建工具演进里也提到按需引入对体积的影响;而把状态持久化到 localStorage 以跨刷新保留时,可与前端缓存策略配合落地。

小结

没有”最好的”状态库,只有”最合适的”。原型和小工具直接用 Zustand;状态高度分散、追求极致细粒度更新选 Jotai;中大型团队协作、接口多且要强约束,Redux Toolkit + RTK Query 更稳。选型时先问自己三个问题:团队规模、状态耦合度、是否大量服务端数据——答案往往就清晰了。

上一篇 大模型推理引擎怎么选:vLLM/SGLang/Ollama
下一篇 Prometheus 告警规则实战:从 Recording Rule 到 Alertmanager 路由