缅怀革命先烈 致敬人民英雄|每年9月30日为烈士纪念日,缅怀革命先烈,致敬人民英雄。山河已无恙,吾辈当自强!

React 状态管理选型:Zustand、Redux Toolkit 与 Jotai

zero
zero 见习用户见习用户
发布于 2026-09-29 02:36 ·15 浏览 ·0 回复

学完这篇你能拿到一套可直接照抄的选型方法:先看清三个库的状态模型差异,再各写一段最小可运行代码,最后按项目规模和数据形态做决定,而不是跟着热度走。

第一步:先用两个问题缩小范围

不要一上来就比 API。先问自己两件事:

  1. 这个状态是服务端数据还是客户端数据? 接口返回的列表、详情一律交给 TanStack Query 或 RTK Query,三个库都不该管。
  2. 状态之间的派生关系复杂吗? 如果只是「一个数组 + 几个操作函数」,Zustand 够用;如果是「A 变 B 变 C,C 还要组合 D」,Jotai 的原子模型更省心;如果要多人协作、要严格规范和时间旅行调试,Redux Toolkit 更稳。

第二步:Zustand 最小示例

import { create } from 'zustand'

export const useCartStore = create((set, get) => ({
  items: [],
  add: (item) => set((s) => ({ items: [...s.items, item] })),
  total: () => get().items.reduce((n, i) => n + i.price, 0),
}))

组件里按需订阅:

const items = useCartStore((s) => s.items)
const add = useCartStore((s) => s.add)

没有 Provider,没有 action type,没有 reducer 样板。需要持久化加 `persist` 中间件,需要 Redux DevTools 加 `devtools` 中间件,都是一行。

注意:选择器如果返回新对象或新数组(比如 `useCartStore((s) => s.items.filter(...))`),每次渲染都会判定为「变了」,Zustand 会抛无限循环警告。解决办法是 `import { useShallow } from 'zustand/react/shallow'` 包一层,或者让选择器只返回原始值。

第三步:Redux Toolkit 最小示例

import { configureStore, createSlice } from '@reduxjs/toolkit'

const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [] },
  reducers: {
    add(state, action) { state.items.push(action.payload) }, // Immer 允许这么写
  },
})
export const { add } = cartSlice.actions
export const store = configureStore({ reducer: { cart: cartSlice.reducer } })

入口文件挂 Provider:

import { Provider } from 'react-redux'
<Provider store={store}><App /></Provider>

组件里取数和派发:

const items = useSelector((s) => s.cart.items)
const dispatch = useDispatch()
dispatch(add({ id: 1, price: 20 }))

RTK 的价值不在「少写代码」,而在统一约定:状态怎么切、异步怎么走(`createAsyncThunk` 或 RTK Query)、谁改了数据(DevTools 时间旅行 + action 日志)。团队超过三五个人、状态跨十几个页面时,这套约束能省下大量争论。

注意:Immer 只在 `createSlice` 的 reducer 内部生效。在 reducer 外面拿到 state 后直接 `state.items.push(...)` 会报错,必须先 `JSON.parse(JSON.stringify())` 或 `structuredClone` 复制一份。

第四步:Jotai 最小示例

import { atom, useAtomValue } from 'jotai'

export const itemsAtom = atom([])
export const totalAtom = atom((get) =>
  get(itemsAtom).reduce((n, i) => n + i.price, 0)
)

function Total() {
  const total = useAtomValue(totalAtom)
  return <span>{total}</span>
}

`totalAtom` 是派生原子,`itemsAtom` 一变它自动重算,且只有用到 `totalAtom` 的组件重渲染。这是 Jotai 相比前两者的核心差异——更新粒度天然细化,不需要手写选择器和 `useShallow`。

注意:`atom()` 必须写在模块顶层。写在组件函数里面,每次渲染都会创建一个全新原子,状态永远对不上,而且很难排查。异步原子配 `<Suspense>` 使用时记得给 fallback。

第五步:按场景拍板

场景选择
个人项目、后台小工具、只想要一个全局 storeZustand
中大型团队、状态多、要调试审计、已用 Redux 生态Redux Toolkit
派生关系复杂、高频更新、编辑器/画布/表单联动Jotai

三条补充建议:

  • 不要三个一起上。真要混,也最多是 RTK 管全局业务数据 + Jotai 管某个高频交互区域,并且中间用一层适配函数隔开。
  • 别把接口数据塞进这些 store。缓存、重试、失效交给 TanStack Query / RTK Query,否则你会手写一遍它们的活。
  • 迁移成本远低于预期:Zustand 和 Jotai 都是几百字节级的库,一个页面里可以先局部试用,跑通再扩大,不需要全量重写。

小结

  • 先用「服务端数据还是客户端数据」「派生关系复杂不复杂」两个问题缩小范围。
  • Zustand:无 Provider、样板最少,注意选择器返回新引用时用 `useShallow`。
  • Redux Toolkit:约束强、工具链全,注意 Immer 只在 reducer 内生效。
  • Jotai:原子 + 派生原子,重渲染粒度最细,注意 atom 定义在模块顶层。
  • 服务端数据一律交给 TanStack Query / RTK Query,不要塞进这三个库。
  • 拿不准就先用 Zustand,真的撞到墙再换,成本可控。
本文转载自 Clara轻量论坛系统 - 轻量级 PHP 论坛系统,原文地址:https://www.leleweb.cn/thread-632.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~