React 那么多状态管理库,到底选哪个?如果非要焊死一个呢?这篇文章解决你的选择困难症
前言
各位 React 开发者们,是不是还在为状态管理头疼?在我的这篇文章中:
有掘友问到:React那么多状态库,能不能直接焊死一个?
![]()
那就简单聊下我的看法(仅供参考)。篇幅比较长,中间的代码示例大家可以跳着阅读。
📊 主流状态管理库分类
1. 客户端状态管理
-
Redux Toolkit(RTK) - 最成熟,企业级首选 -
Zustand- 轻量简洁,API 友好 -
Jotai- 原子化状态,React 风格 -
Recoil- Facebook 出品,实验性 -
Valtio- 代理基础,可变语法
2. 服务端状态管理
-
TanStack Query (React Query)- 异步数据王者 -
SWR- Vercel 出品,轻量 -
RTK Query- Redux 生态内
3. 全栈/框架集成
-
Next.js- 内置多种方案 -
Remix- 基于 loader/action -
Nuxt (Vue)- 类比参考
🎯 我的建议:焊死这个组合
对于大多数项目,如果非要焊死一个的话,我推荐:Zustand + TanStack Query。React 太多状态管理库了,如果非要焊死一个,我目前推荐这个王炸组合。
🌈 为什么选择这个组合
一、先搞懂:为什么要分开处理两种状态?
在开始安利组合之前,我们得先明确一个核心认知:React 项目中的状态,从来都不是一锅炖的,而是分为两种截然不同的类型,需要区别对待。
-
客户端本地状态(UI 状态) 比如:按钮的禁用状态、侧边栏的展开 / 折叠、导航栏的当前选中项、用户的本地偏好设置等。这类状态的特点是:
同步更新、无需缓存、仅在客户端生效、数据量较小。 -
服务端异步状态(接口数据) 比如:从后端获取的用户列表、文章数据、商品信息等。这类状态的特点是:
异步获取、需要处理 loading/error 状态、需要缓存、可能需要后台刷新、支持分页 / 无限加载。
过去我们总想着用一套方案搞定所有状态,结果就是既要又要还要,最后搞得不伦不类。而 Zustand + TanStack Query 的组合,正是精准切中了这两种状态的需求,各司其职、强强联合。
二、 Zustand:客户端状态管理的「极简天花板」
Zustand 是一款轻量、简洁、API 友好的客户端状态管理库,它的核心理念就是少即是多—— 没有繁琐的概念,没有多余的模板代码,甚至不需要 Provider 包裹整个应用,上手即用。
1. 核心优势:为什么放弃 Redux 选择 Zustand?
- 🚀 代码量减少 70% :无需写 Action、Reducer,无需配置 Provider,直接创建 Store 即可使用。
- 🎉 无需 Provider 包裹:告别顶层嵌套的 Provider 地狱,尤其是在大型项目中,能极大简化组件树结构。
- 🔒 TypeScript 支持完美:内置 TypeScript 类型推导,无需额外写大量类型声明,写起来丝滑流畅。
- ⭐ JavaScript 无缝兼容:无需额外配置类型,原生 JS 写起来丝滑流畅,新手也能快速上手。
- 💪 足够应对 95% 的客户端状态需求:支持中间件、持久化、状态切片,扩展性拉满,小型项目和中型项目都能 hold 住。
- 📦 超小体积:核心体积不到 1KB,对项目打包体积几乎没有影响,堪称「轻量王者」。
2. 代码示例:5 分钟上手 Zustand
第一步:安装依赖
npm install zustand
# 或 yarn add zustand
# 或 pnpm add zustand
第二步:创建第一个 Store
我们来写一个最简单的计数器,感受一下 Zustand 的简洁:
// src/store/count.store.js
import { create } from 'zustand';
// 创建计数器 Store
const useCountStore = create((set) => ({
// 定义状态数据
count: 0,
// 定义修改状态的方法(无需 Action,直接修改)
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
// 支持传入参数修改状态
setCount: (num) => set({ count: num }),
}));
export default useCountStore;
// scr/App.jsx
import useCountStore from './store/count.store.js'
export default function App() {
return (
<div>
<h1>Count: {useCountStore((state) => state.count)}</h1>
</div>
)
}
![]()
第三步:在组件中使用 Store
无需任何额外配置,直接导入使用,就是这么简单:
// src/components/CountComponent.jsx
import useCountStore from '../store/count.store';
const CountComponent = () => {
// 按需获取状态和方法(支持解构,不会触发不必要的重渲染)
const count = useCountStore((state) => state.count);
const { increase, decrease, reset } = useCountStore();
return (
<div style={{ padding: '20px', border: '1px solid #eee', borderRadius: '8px' }}>
<h3 style={{ color: '#1890ff' }}>Zustand 计数器示例</h3>
<p style={{ fontSize: '24px', margin: '20px 0' }}>当前计数:{count}</p>
<div>
<button
onClick={increase}
style={{ marginRight: '10px', padding: '8px 16px', cursor: 'pointer' }}
>
+1
</button>
<button
onClick={decrease}
style={{ marginRight: '10px', padding: '8px 16px', cursor: 'pointer' }}
>
-1
</button>
<button
onClick={reset}
style={{ padding: '8px 16px', cursor: 'pointer', backgroundColor: '#f5f5f5' }}
>
重置
</button>
</div>
</div>
);
};
export default CountComponent;
![]()
进阶示例: Zustand 持久化(本地存储用户偏好)
如果需要将状态持久化到 localStorage(比如用户的侧边栏偏好),Zustand 也能轻松实现,只需借助内置的中间件:
// src/store/ui.store.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
// 创建 UI 状态 Store,并开启持久化
const useUiStore = create(
persist(
(set) => ({
// 侧边栏展开状态
sidebarCollapsed: false,
// 切换侧边栏状态
toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
}),
{
// 持久化的 key(用于 localStorage 中存储的键名)
name: 'ui-preferences',
}
)
);
export default useUiStore;
使用起来和普通 Store 毫无区别,但是状态会自动同步到 localStorage,页面刷新后也不会丢失,个人觉得还是挺方便的。
三、 TanStack Query:服务端状态管理的「异步王者」
如果说 Zustand 是客户端状态的极简天花板,那么 TanStack Query(原 React Query)就是服务端状态的异步王者。
它的核心作用是:帮你封装了所有服务端数据处理的繁琐逻辑,让你像使用本地状态一样使用异步接口数据。你再也不用手动处理 loading、error、缓存、重试这些问题,只需专注于编写接口请求函数即可。
1. 核心优势:为什么选择 TanStack Query?
- 🚀 自动缓存:请求的数据会自动缓存,相同的请求不会重复发送,极大减少接口请求次数。
- 🎉 自动处理 loading/error 状态:内置 loading、error、data 状态,无需手动声明和更新。
- 💪 后台数据同步:支持后台刷新数据,页面在前台时自动更新最新数据,无需用户手动刷新。
- 📦 内置分页 / 无限加载 / 乐观更新:提供丰富的 Hooks 支持复杂的异步数据场景,无需自己造轮子。
- 🔄 自动重试:请求失败时可以配置自动重试,提高接口的容错性。
- 🧰 强大的 DevTools:配套的开发者工具,能清晰看到请求的缓存、状态、历史记录,调试更方便。
2. 代码示例:5 分钟上手 TanStack Query
第一步:安装依赖
npm install @tanstack/react-query @tanstack/react-query-devtools
# 或 yarn add @tanstack/react-query @tanstack/react-query-devtools
# 或 pnpm add @tanstack/react-query @tanstack/react-query-devtools
第二步:全局配置 TanStack Query
首先需要在项目入口文件中配置 QueryClient 和 QueryClientProvider,这是唯一需要全局配置的步骤:
// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
// 创建 QueryClient 实例
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 默认开启缓存,5 分钟内不重复请求
staleTime: 5 * 60 * 1000,
// 请求失败时自动重试 3 次
retry: 3,
// 关闭无限加载(可选)
refetchOnWindowFocus: false,
},
},
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<QueryClientProvider client={queryClient}>
<App />
{/* 挂载 DevTools(开发环境开启,生产环境可移除) */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
第三步:封装接口请求 Hook
我们来封装一个获取待办事项的 Hook,感受一下 TanStack Query 的强大:
// src/api/todos.api.js
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// 1. 定义接口请求函数
const fetchTodos = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos');
if (!response.ok) {
throw new Error('获取待办事项失败');
}
return response.json();
};
// 2. 定义新增待办事项的函数(纯 JavaScript)
const addTodo = async (newTodo) => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newTodo),
});
if (!response.ok) {
throw new Error('新增待办事项失败');
}
return response.json();
};
// 3. 封装获取待办事项的 Hook(使用 useQuery,纯 JavaScript)
export const useTodosQuery = () => {
return useQuery({
// queryKey:缓存的唯一标识,必须是数组类型(支持依赖项传递)
queryKey: ['todos'],
// queryFn:接口请求函数
queryFn: fetchTodos,
});
};
// 4. 封装新增待办事项的 Hook(使用 useMutation,处理 POST/PUT/DELETE 请求)
export const useAddTodoMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addTodo,
// 新增成功后,自动刷新待办事项列表(乐观更新)
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
};
第四步:在组件中使用接口 Hook
无需手动处理 loading 和 error,直接解构使用即可,代码简洁到飞起:
// src/components/TodoComponent.jsx
import React, { useState } from 'react';
import { useTodosQuery, useAddTodoMutation } from '../api/todos.api';
const TodoComponent = () => {
const [title, setTitle] = useState('');
// 获取待办事项数据
const { data: todos, isLoading, isError, error } = useTodosQuery();
// 新增待办事项
const { mutate: addTodo, isPending: isAdding } = useAddTodoMutation();
// 处理新增待办事项提交
const handleSubmit = (e) => {
e.preventDefault();
if (!title.trim()) return;
addTodo({ title, completed: false });
setTitle('');
};
// 加载中状态
if (isLoading) {
return <div style={{ padding: '20px' }}>正在获取待办事项...</div>;
}
// 错误状态
if (isError) {
return <div style={{ padding: '20px', color: '#ff4d4f' }}>获取失败:{error.message}</div>;
}
return (
<div style={{ padding: '20px', border: '1px solid #eee', borderRadius: '8px', marginTop: '20px' }}>
<h3 style={{ color: '#1890ff' }}>TanStack Query 待办事项示例(JSX)</h3>
{/* 新增待办事项表单 */}
<form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="请输入待办事项"
style={{ padding: '8px', width: '300px', marginRight: '10px' }}
/>
<button
type="submit"
disabled={isAdding}
style={{ padding: '8px 16px', cursor: 'pointer', backgroundColor: '#1890ff', color: '#fff', border: 'none', borderRadius: '4px' }}
>
{isAdding ? '新增中...' : '新增待办'}
</button>
</form>
{/* 待办事项列表 */}
<div>
<h4>待办列表(前 10 条)</h4>
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
{todos?.slice(0, 10).map((todo) => (
<li
key={todo.id}
style={{
padding: '10px',
borderBottom: '1px solid #f5f5f5',
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? '#999' : '#333',
}}
>
{todo.title}
</li>
))}
</ul>
</div>
</div>
);
};
export default TodoComponent;
运行代码后,你会发现:请求自动发送、加载状态自动处理、新增数据后列表自动刷新、相同请求不会重复发送 —— 这一切,都是 TanStack Query 帮你做好的,你只需要专注于业务逻辑即可。
四、 王炸组合落地:项目结构最佳实践
看完了两个库的单独使用,我们再来看看如何在实际项目中整合 Zustand + TanStack Query,打造一个清晰、可维护的项目结构。
src/
├── store/ # Zustand 客户端状态存储目录
│ ├── auth.store.js # 认证相关状态(登录状态、用户信息)
│ ├── ui.store.js # UI 相关状态(侧边栏、主题、导航)
│ └── index.js # Store 导出汇总(方便组件导入)
├── api/ # TanStack Query 接口 Hook 目录
│ ├── todos.js # 待办事项相关接口
│ ├── users.js # 用户相关接口
│ └── index.js # 接口 Hook 导出汇总
├── components/ # 公共组件目录
├── pages/ # 页面组件目录
└── App.jsx # 根组件
🚀 快速决策指南
可能有人会问:我的项目很小,需要用这套组合吗?我的项目是大型企业级项目,这套组合够用吗?
别急,我让DeepSeek给大家整理了一份懒人快速决策指南,对应不同场景选择最合适的方案:
-
超简单状态(单个组件内、无需共享):直接用
useState即可,无需引入任何状态库,简单直接。 -
小型项目 / 简单共享状态(少量组件共享状态):可以用
React Context + useReducer,或者直接用 Zustand(上手更快,代码更简洁)。 -
中型项目(推荐,90% 的项目场景):直接焊死
Zustand + TanStack Query,开发体验最佳,覆盖 99% 的场景,后期维护成本低。 -
大型企业级项目(需要强架构、可追溯、团队协作):可以选择
Redux Toolkit + RTK Query,支持时间旅行调试、丰富的中间件生态,适合对架构有严格要求的大型项目。 -
超极简需求(只需要原子化状态):可以选择
Jotai或nanostores,原子化状态管理,按需更新,体积更小。
📝 具体落地建议
// 1. 安装核心依赖
"dependencies": {
"zustand": "^4.0.0",
"@tanstack/react-query": "^5.0.0",
"@tanstack/react-query-devtools": "^5.0.0"
}
// 2. 项目结构
src/
├── store/ # Zustand stores
│ ├── auth.store.ts
│ ├── ui.store.ts
│ └── index.ts
├── api/ # TanStack Query hooks
│ ├── todos.ts
│ └── users.ts
└── components/
🔄 迁移策略
如果你现在用 Redux,逐步迁移:
- 新功能用
Zustand - 旧功能保持
Redux - 两者可以共存
💡 黄金法则
-
先判断状态类型:
- 服务器数据?→
TanStack Query - 客户端 UI 状态?→
Zustand - 表单状态?→
React Hook Form + Zustand
- 服务器数据?→
-
避免过度设计:
- 能用
useState就别用状态库 - 组件内状态优先
- 共享状态才提升
- 能用
-
技术选型标准:
- 团队熟悉度
- 维护活跃度
- TypeScript 支持
- Bundle 大小
🎖️ 最终答案
非要焊死的话,那我推荐这个组合:Zustand + TanStack Query
这个组合能覆盖:
- ✅ 客户端状态(Zustand)
- ✅ 服务端状态(TanStack Query)
- ✅ 表单状态(React Hook Form)
- ✅ URL 状态(React Router)
对于 90% 的 React 项目,这套组合是最佳实践。除非你有特殊需求(如需要 Redux 中间件生态或时间旅行调试)。
结语
到这里,相信大家已经对 Zustand + TanStack Query 这套王炸组合有了全面的了解。
这套组合的核心魅力就在于:简洁、高效、各司其职。Zustand 搞定客户端本地状态,让你告别繁琐的 Provider 和模板代码;TanStack Query 搞定服务端异步数据,让你告别手动处理 loading/error/ 缓存的烦恼。
祝大家编码愉快,少写 bug,多摸鱼~ 🚀