前端状态管理方案对比:RTK、Zustand 与 Pinia 怎么选
"状态管理"是前端面试与架构讨论的高频词,但很多团队其实把两件不同的事混在了一起:客户端 UI 状态(弹窗开合、表单、主题)与服务端数据状态(从接口拉取的用户、订单、列表)。前者需要的是"存放与共享状态的容器",后者需要的往往是"数据获取与缓存层"。分清楚这一点,选型会立刻清晰很多。
本文是前端搭建分类的对比类内容。若想先了解承载这些方案的框架,可阅读React 19 指南与Vue 3 指南;框架整体的横向对比见前端框架对比 2026。
先分清:客户端状态 vs 服务端状态
TanStack Query 文档里把问题讲得很清楚:服务端状态"持久化在你不拥有的远程位置、需要异步 API 读写、可能被其他人修改、容易过期"。传统状态管理库擅长管理客户端状态,却不太擅长异步与服务端状态——缓存、请求去重、后台刷新、失效判断这些恰恰是最难的部分。用错工具,会越写越复杂。
Redux Toolkit:完整但偏重的"标准答案"
Redux Toolkit(RTK)是 Redux 的官方推荐写法,目标就是解决"配置复杂、要装很多包、样板代码多"三大痛点:
configureStore()内置默认中间件与 DevTools 支持,自动合并 slice reducer;createSlice()把 reducer、action 与 action type 一次性生成,配合 Immer 可直接写"看似可变"的更新;createAsyncThunk管理 pending/fulfilled/rejected 三个异步状态;- RTK Query 作为内置的数据获取与缓存层,
createApi定义端点、fetchBaseQuery封装请求,可省去手写 fetch 逻辑。
适合:团队规模大、需要强规范与时间旅行调试、状态结构复杂(如规范化数据)的应用。代价是概念多、上手成本高。
Zustand:小而快的 hooks 方案
Zustand 以"小、快、可扩展"著称,基于简化版 Flux 原则,API 极其精简:create 定义 store,组件里用 selector 订阅,默认不需要 Provider 包裹,样板代码接近零:
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}))
// 组件内:const count = useStore(s => s.count)
配合 selector 还能控制渲染粒度,避免无关状态变化触发整棵子树重渲。适合中小型应用、追求极简与高性能的团队。
Pinia:Vue 生态的官方推荐
Pinia 是 Vue 官方推荐的状态库,取代了 Vuex。defineStore 定义 store,天然具备 state/getters/actions 结构,类型推断完善,DevTools 提供时间轴与时间旅行,还支持服务端渲染与插件扩展(详见 Pinia 官方文档对 Vuex 的对比:取消了 mutations、扁平化 store、无魔法字符串)。
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: { double: (s) => s.count * 2 },
actions: { increment() { this.count++ } },
})
适合:Vue 3 项目,尤其是与Vue 3的组合式 API 搭配。
Jotai:原子化状态
Jotai 采用"原子(atom)"模型:每个状态是一个 atom,通过 useAtom 读写,派生原子自动做依赖追踪与细粒度更新,心智模型接近 React 内部机制。适合需要高度可组合、希望状态天然模块化且不引入全局 Provider 争议的场景。
TanStack Query:服务端状态的最佳归宿
TanStack Query(原 React Query)定位是"服务端状态":useQuery + queryKey + queryFn 即可获得加载/错误/数据三态,自动缓存、请求去重、后台刷新、失效后重取、分页与内存管理。它能显著减少"手写 loading 状态 + useEffect 拉数据"的模板代码:
const { isPending, error, data } = useQuery({
queryKey: ['repoData'],
queryFn: () => fetch(url).then((res) => res.json()),
})
最佳实践:客户端状态交给 Zustand/Pinia/Jotai,服务端数据交给 TanStack Query(或 RTK Query),两者职责互补、互不冲突。
方案对比表
| 方案 | 定位 | 学习成本 | 样板代码 | 典型适用 |
|---|---|---|---|---|
| Redux Toolkit | 完整客户端状态 + RTK Query 数据层 | 高 | 中 | 大型应用、规范化数据、强规范团队 |
| Zustand | 极简客户端状态 | 低 | 极少 | 中小应用、追求性能与轻量 |
| Pinia | Vue 官方客户端状态 | 低 | 少 | Vue 3 生态 |
| Jotai | 原子化客户端状态 | 中 | 少 | 细粒度更新、可组合状态 |
| TanStack Query | 服务端数据缓存 | 中 | 少 | 接口密集型应用,与任何 UI 状态库搭配 |
选型建议
- 先问数据从哪来:大部分"状态"其实是接口数据,优先考虑 TanStack Query / RTK Query;
- React 团队:轻量选 Zustand,重度选 Redux Toolkit;复杂数据获取直接上 TanStack Query;
- Vue 团队:默认 Pinia,数据层同样可配 TanStack Query;
- 别过度设计:简单应用用组件内 state + Context 就够,状态库是"需要时才引入"的工具。
一次真实的选型过程
用一个具体场景来说明:一个 4 人团队要重写一个数据看板,页面里既有用户筛选条件、主题切换这类客户端状态,也有从三个接口聚合的报表数据。第一版他们把所有东西都塞进一个 Redux store,结果每次报表刷新都会触发筛选条件组件的重渲染,接口请求也经常重复发出。
后来他们按本文的边界重新划分:筛选条件和主题用 Zustand 管理,报表数据交给 TanStack Query 缓存,RTK 的 reducer 全部删除。改动之后,页面交互的响应速度明显提升,接口重复请求消失,代码量也少了一大截。整个重构只花了一天,因为边界清楚了,改动范围很好控制。
这个例子也说明一个常被忽略的点:状态管理方案之间不是「非此即彼」,Zustand、TanStack Query 甚至 Context 可以同时出现在一个项目里,关键是各自管好自己擅长的部分。
16IDC 观察
对建站与 SaaS 团队而言,状态管理的真正成本不在库本身,而在"状态边界是否清晰"。把客户端状态与服务端状态分层管理后,代码更容易测试、更易交接、也更能承受业务增长。落地时建议在前端搭建分类的组件规范基础上,先沉淀一份"什么状态放哪里"的团队约定。
一个实用的起步规则是:组件私有的 UI 状态用 useState;跨组件共享的全局 UI 状态用轻量库(Zustand/Pinia);任何来自接口的数据,一律走数据层(TanStack Query / RTK Query)。把这条规则写进团队的 Code Review 清单,比争论「用哪个库」更有价值。
原文来源:https://redux-toolkit.js.org/introduction/getting-started