什么是状态管理,它解决什么问题
React 的数据流是单向的:数据通过 Props 从父组件流向子组件。这在小型应用中很好用,但随着应用规模扩大,会遇到两个典型痛点:
Prop Drilling(属性透传)
同一份数据需要从顶层组件一层一层往下传,中间层组件本身并不使用这个数据,却不得不接收并转发它。组件层级越深,代码越难维护。
App(持有 user 状态)
└─ Layout
└─ Sidebar
└─ UserAvatar(需要 user)← 要传三层 props
跨组件状态同步困难
当两个没有父子关系的组件需要访问同一份数据时,只能把状态提升到它们共同的祖先组件,然后再层层传下去。状态越分散,同步越容易出 bug。
状态管理库的解法
把共享状态提取到组件树之外的独立 Store,任何组件都可以直接订阅和修改,不需要层层传递 Props。
Store(独立于组件树)
↗ ↗
ComponentA ComponentB
React 主流状态管理方案对比
| 库 | 定位 | 包大小 | 学习曲线 |
|---|---|---|---|
| Zustand | 轻量、直觉式 API | ~1 KB | 低 |
| Jotai | 原子化状态,细粒度订阅 | ~3 KB | 低 |
| Redux Toolkit | 功能完整,适合大型项目 | ~20 KB | 高 |
Zustand
Zustand 是目前最流行的轻量状态管理库,API 极简,和 Vue 的 Pinia 非常相似。
创建 Store:
import { create } from "zustand";
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
在组件中使用:
function Counter() {
const { count, increment, decrement } = useCounterStore();
return (
<div>
<p>当前计数:{count}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
);
}
异步操作:
const useUserStore = create((set) => ({
user: null,
loading: false,
fetchUser: async (id) => {
set({ loading: true });
const data = await fetchUserById(id);
set({ user: data, loading: false });
},
}));
Zustand 不需要 Provider 包裹,Store 是全局单例,直接在任何组件里调用即可。
Jotai
Jotai 采用**原子化(Atom)**的设计思路:把状态拆成最小单元(atom),组件只订阅自己需要的那部分,变更时只有相关组件重渲染,性能更精确。
import { atom, useAtom } from "jotai";
// 定义原子
const countAtom = atom(0);
const nameAtom = atom("Alice");
// 派生原子(类似 computed)
const doubleCountAtom = atom((get) => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
);
}
Jotai 适合状态之间有复杂依赖关系、需要精细控制渲染性能的场景。
状态存在哪里
Jotai(以及 Zustand、Redux)默认把状态存在浏览器内存里,本质是"有订阅能力的全局变量":
- 页面刷新 → 状态重置为初始值
- 关闭标签页 → 销毁
- 不同标签页 → 互相隔离,不共享
需要持久化时,使用 atomWithStorage 显式接入存储后端:
import { atomWithStorage } from "jotai/utils";
// 存到 localStorage(刷新后仍保留)
const themeAtom = atomWithStorage("theme", "light");
// 存到 sessionStorage(关闭标签页后清除)
const tokenAtom = atomWithStorage("token", null, sessionStorage);
Zustand 用 persist 中间件实现同样效果:
import { persist } from "zustand/middleware";
const useStore = create(
persist(
(set) => ({ theme: "light", setTheme: (t) => set({ theme: t }) }),
{ name: "app-settings" } // localStorage key
)
);
各存储位置对比:
| 存储位置 | 生命周期 | 容量 | 典型用途 |
|---|---|---|---|
| 内存(默认) | 刷新即丢失 | 无限制 | 弹窗状态、筛选条件、购物车 |
| localStorage | 永久(手动清除前) | ~5MB | 主题、语言偏好、用户设置 |
| sessionStorage | 关闭标签页清除 | ~5MB | 临时 token、表单草稿 |
| IndexedDB | 永久,大容量 | 几百MB+ | 离线缓存、大量结构化数据 |
| Cookie | 可设过期时间 | ~4KB | 认证 token(随请求发给服务端) |
与后端单例模式的对比
前端状态管理库的本质和后端单例对象共享状态思路一致:atom 文件定义在模块顶层,JS 模块系统天然是单例的,不同组件 import 同一个 atom 拿到的是同一个引用。
// store/counter.js —— 定义一次,全局共享
export const countAtom = atom(0);
// ComponentA.jsx
import { countAtom } from "../store/counter";
const [count, setCount] = useAtom(countAtom); // 订阅同一个 atom
// ComponentB.jsx
import { countAtom } from "../store/counter";
const [count, setCount] = useAtom(countAtom); // 状态共享,A 改了 B 自动更新
与后端单例的区别在于多了一层响应式订阅:atom 变化时自动推送给所有订阅者,不需要主动轮询。
可以把状态管理库理解为:JS 模块单例 + 发布订阅(Pub/Sub)
Redux Toolkit
Redux 是 React 最老牌的状态管理方案,曾经因为样板代码多而饱受诟病。Redux Toolkit(RTK)是官方推出的简化版,大幅减少了冗余代码。
import { createSlice, configureStore } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
},
});
const store = configureStore({
reducer: { counter: counterSlice.reducer },
});
import { useSelector, useDispatch } from "react-redux";
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(counterSlice.actions.increment())}>
{count}
</button>
);
}
Redux 的优势在于完善的 DevTools、中间件生态,以及对大型团队友好的严格数据流约束。
如何选型
- 新项目、中小规模 → Zustand,上手快,代码简洁
- 需要细粒度性能优化 → Jotai,原子化订阅避免多余渲染
- 大型企业项目、多人协作 → Redux Toolkit,规范严格,生态完善
- 极简场景(无需第三方依赖) → React Context,但要注意性能问题
与 Vue Pinia 的对比
| 特性 | Pinia (Vue) | Zustand (React) |
|---|---|---|
| 定义 Store | defineStore() |
create() |
| 读取状态 | store.count |
useStore((s) => s.count) |
| 修改状态 | 直接赋值 / action | set() 函数 |
| 异步 action | 直接 async 函数 | 直接 async 函数 |
| DevTools | ✅ Vue DevTools | ✅ Redux DevTools(可选插件) |
| Provider 包裹 | 不需要 | 不需要 |
两者设计理念非常接近,从 Pinia 切换到 Zustand 的学习成本很低。
与 Java 后端的对比
Java 后端没有专门的"状态管理"分类,因为后端天然倾向于无状态设计——状态通常外置到数据库或 Redis,而不是放在进程内存里。但 Jotai 的两个核心能力在 Java 里都有对应:
单例共享(对应 Jotai 的模块单例部分)
Spring Bean 默认就是单例,注入到哪里拿到的都是同一个对象:
@Service // 默认单例,全局共享
public class CounterService {
private int count = 0;
public void increment() { count++; }
public int getCount() { return count; }
}
响应式订阅(对应 Jotai 的 Pub/Sub 部分)
Spring 内置事件系统:
// 发布
applicationEventPublisher.publishEvent(new CountChangedEvent(newCount));
// 订阅
@EventListener
public void onCountChanged(CountChangedEvent event) { ... }
最接近 Jotai atom 的是 RxJava 的 BehaviorSubject,它持有当前值并在值变化时通知所有订阅者:
// 定义状态(类比 atom)
BehaviorSubject<Integer> countState = BehaviorSubject.createDefault(0);
// 修改(类比 setCount)
countState.onNext(countState.getValue() + 1);
// 订阅(类比 useAtom)
countState.subscribe(count -> System.out.println("count: " + count));
为什么后端没有专门的状态管理库
| 维度 | 前端 | 后端 |
|---|---|---|
| 状态生命周期 | 跟随浏览器页面 | 跟随进程(可能随时重启) |
| 多实例 | 单个页面,状态天然集中 | 水平扩展多实例,进程内状态无法跨实例共享 |
| 状态持久化 | localStorage / IndexedDB | 数据库 / Redis |
| 状态同步 | 组件间(同一进程) | 服务间(跨进程/跨机器) |
后端一旦水平扩展,进程内存里的状态每个实例各自一份,互相不同步,所以共享状态直接存 Redis,不依赖进程内存。
概念对照表
| Jotai | Java 对应 |
|---|---|
| atom(单例状态) | Spring Bean(单例) |
useAtom(订阅) |
@EventListener / RxJava subscribe |
| 派生 atom(computed) | RxJava map / combineLatest |
atomWithStorage |
Redis / JPA |
| Jotai 整体 | RxJava BehaviorSubject 最接近 |