ccy

React 状态管理选型指南:从 useState 到 Jotai 的演进之路

·0 次阅读·
React前端状态管理

状态管理的本质

React 状态管理本质上解决的是两个问题:状态共享状态同步。当组件树深度超过三层、或者多个兄弟节点需要读取同一份数据时,单纯依靠 Props Drilling 会导致中间组件传递大量无关 Props,出现所谓的「Props 穿透」问题。

本文从实际工程角度,分析四种主流方案的适用边界。

1. useState — 最基础的原子化状态

const [count, setCount] = useState(0)
const [user, setUser] = useState<User | null>(null)

适用场景:组件私有状态、表单输入、UI 开关(弹窗/菜单展开)。

限制:状态提升到父组件后,任何子组件的状态更新都会触发整个子树重渲染。解决方案是配合 React.memo + useCallback 手动优化,但这引入了心智负担——开发者需要时刻追踪组件的引用稳定性。

最佳实践:将状态按「变化频率」拆分。高频变化的状态(如输入框值)独立 useState,低频变化的状态(如列表数据)放到另一个 useState,避免高频更新引发低频数据的连带重渲染。

2. useReducer — 复杂状态逻辑的归约器模式

type Action =
  | { type: "ADD_TODO"; payload: Todo }
  | { type: "TOGGLE_TODO"; payload: number }

function todoReducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, action.payload]
    case "TOGGLE_TODO":
      return state.map(t =>
        t.id === action.payload
          ? { ...t, done: !t.done }
          : t
      )
  }
}

适用场景:状态更新逻辑复杂、多个子值相互依赖、需要清晰记录状态变更历史。

核心优势:将状态更新逻辑从组件中抽离为纯函数(Reducer),状态变更可追踪、可测试、可序列化。配合 Context 可以实现局部状态共享,而不引入外部依赖。

陷阱:Reducer 必须是纯函数——不能有副作用,不能调用 Math.random()Date.now()。副作用应该在 dispatch 之前或之后通过 useEffect 处理。

3. Zustand — 轻量级全局状态库

import { create } from "zustand"

interface BearStore {
  bears: number
  increase: () => void
  reset: () => void
}

const useBearStore = create<BearStore>((set) => ({
  bears: 0,
  increase: () => set((state) => ({ bears: state.bears + 1 })),
  reset: () => set({ bears: 0 }),
}))

适用场景:跨组件/跨路由的全局状态(用户信息、主题设置、购物车)。

相比 Redux 的优势:无 Provider 包裹、无 Action Creator 样板代码、无必要的中间件配置。API 设计借鉴了 React Hooks 的哲学——create() 返回一个 Hook,直接在任何组件中调用。

性能机制:Zustand 默认使用 Object.is 做浅比较,只有 selector 返回值变化时才会触发组件重渲染。这意味着可以精确控制组件对状态的订阅粒度:

// 只订阅 bears 字段,bears 不变则不重渲染
const bears = useBearStore((s) => s.bears)

4. Jotai — 原子化状态的原生体验

import { atom, useAtom } from "jotai"

const countAtom = atom(0)
const doubledAtom = atom((get) => get(countAtom) * 2)

function Counter() {
  const [count, setCount] = useAtom(countAtom)
  const doubled = useAtomValue(doubledAtom)
  return <div>{count} × 2 = {doubled}</div>
}

适用场景:需要细粒度状态派生、递归依赖追踪、与 React Suspense 集成的场景。

核心概念:Atom 是最小状态单元,可以通过 get 函数派生新的 Atom。派生 Atom 自动追踪依赖树——当依赖的 Atom 变化时,只有直接和间接依赖该 Atom 的组件会重渲染,无需手动优化。

与 Zustand 的差异:Zustand 是 Store-centric(中心化存储),Jotai 是 Atom-centric(原子化单元)。前者适合「全局有一个 store」的场景,后者适合「状态分散在不同组件子树中」的场景。

选型决策矩阵

方案学习成本样板代码性能适用规模
useState单组件
useReducer单组件复杂逻辑
Zustand中小型全局状态
Jotai分散式状态依赖

总结

没有银弹。我的建议是:优先用 useState,Hold 不住了再考虑外部库。状态共享的复杂度应该驱动选型,而非预先把所有状态塞进全局 Store。对于个人网站这类项目,useState + useReducer 已经够用——这个网站的 Header 菜单展开就是用 useState 实现的。

评论

发表评论