React 状态管理选型指南:从 useState 到 Jotai 的演进之路
状态管理的本质
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 实现的。