状态管理实战
组件树越来越深时,"把状态放到组件里"会迅速失控:兄弟组件要共享、跨页面要共享、历史要回溯。状态管理库就是把这些共享状态集中治理,让数据流清晰可预测。
一、为什么需要状态管理
| 困境 | 说明 |
|---|---|
| 跨组件共享 | 兄弟/远亲组件需要同一份数据 |
| 状态提升 | 提到最外层 props 层层透传,中间组件被"穿透" |
| 数据一致性 | 多处副本容易不同步 |
| 可调试性 | 状态分散,无法回放和追溯 |
text
状态提升的痛点:
App
└─ Page(只是"传话筒",不关心数据)
└─ 组件 A(要 count)← props 一级级传下来
└─ 组件 B(也要 count)← 只能再传一份
→ 中间层被迫接收和转发大量 props
全局 store 的解法:
App ── subscribe
├─ 组件 A(直接用 store 里的 count)
└─ 组件 B(直接用同一个 count)状态分为两类,处理方式不同:
| 类型 | 例子 | 存放位置 |
|---|---|---|
| 局部状态 | 表单输入、开关 | 组件内 state |
| 全局共享状态 | 用户信息、购物车、主题 | 状态管理库 |
| 服务端状态 | 接口返回的列表、详情 | 缓存库(TanStack Query 等) |
二、Redux 核心概念
Redux 的三条铁律:单一数据源、state 只读、用纯函数修改。
| 概念 | 职责 | 规则 |
|---|---|---|
| store | 唯一的全局状态容器 | 整个应用只有一个 |
| state | store 里存的数据 | 不可变,只能整体替换 |
| action | 描述"发生了什么"的普通对象 | 必须有 type 字段 |
| reducer | 根据 action 计算新 state 的纯函数 | 不修改原 state,返回新对象 |
| dispatch | 派发 action 的唯一入口 | 组件通过它触发更新 |
javascript
import { createStore } from "redux";
// 1. 定义 action(通常用 action creator 工厂函数)
const increment = () => ({ type: "counter/increment" });
const setCount = (value) => ({ type: "counter/set", payload: value });
// 2. 定义 reducer:纯函数,(oldState, action) => newState
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case "counter/increment":
return { ...state, count: state.count + 1 }; // 不可变更新
case "counter/set":
return { ...state, count: action.payload };
default:
return state;
}
}
// 3. 创建 store 并订阅
const store = createStore(counterReducer);
store.subscribe(() => console.log("新状态:", store.getState()));
// 4. 派发 action
store.dispatch(increment());
store.dispatch(setCount(10));2.1 纯函数与不可变更新
javascript
// 纯函数:同样的输入永远同样的输出,无副作用
// 不可变更新:绝不修改原对象,返回新对象
// 错误:直接修改原 state(React 无法感知变化,且破坏时间旅行)
state.count += 1;
state.list.push(item);
// 正确:展开语法生成新对象
{ ...state, count: state.count + 1 }
{ ...state, list: [...state.list, item] }不可变更新的价值:引用变了才能触发 diff 和视图更新;也能安全地做撤销、时间旅行调试。
三、Redux 数据流
数据流是单向循环的:
text
用户交互(点击按钮)
→ dispatch(action) 派发动作
→ reducer(state, action) 纯函数计算新 state
→ store 保存新 state
→ 订阅的组件收到通知,重新渲染javascript
// 组件中的完整流程
import { useSelector, useDispatch } from "react-redux";
function Counter() {
const count = useSelector((s) => s.count); // 从 store 读取(自动订阅)
const dispatch = useDispatch(); // 拿到 dispatch
return <button onClick={() => dispatch(increment())}>计数:{count}</button>;
}| 原则 | 违背的后果 |
|---|---|
| reducer 必须纯 | 多次调用结果不一致,时间旅行失效 |
| 单一数据源 | 多份状态难同步,调试困难 |
| 单向数据流 | 数据流向混乱,不可预测 |
四、中间件
中间件夹在 dispatch 与 reducer 之间,拦截 action 做增强:日志、异步、路由跳转。
text
dispatch(action)
→ 中间件链(logger → thunk → …)
→ reducer4.1 redux-thunk:处理异步
javascript
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
// action creator 返回函数(thunk)而非对象,函数里可以发异步请求
const fetchUsers = () => async (dispatch) => {
dispatch({ type: "users/loading" }); // 开始:置 loading
try {
const res = await fetch("/api/users");
const data = await res.json();
dispatch({ type: "users/loaded", payload: data }); // 成功
} catch (err) {
dispatch({ type: "users/error", payload: err.message }); // 失败
}
};
const store = createStore(rootReducer, applyMiddleware(thunk));
store.dispatch(fetchUsers());4.2 redux-logger:开发期日志
javascript
import { createLogger } from "redux-logger";
const store = createStore(rootReducer, applyMiddleware(thunk, createLogger()));
// 每次 dispatch 在控制台打印:前状态 → action → 后状态| 中间件 | 职责 |
|---|---|
| redux-thunk | 让 action 支持函数,处理异步 |
| redux-saga | 用 generator 编排复杂副作用 |
| redux-logger | 打印每个 action 与状态变化 |
| redux-persist | 把 store 持久化到 localStorage |
五、Redux Toolkit
Redux 官方推荐写法,内置 immer、thunk、devtools,样板代码大幅减少:
javascript
import { createSlice, configureStore } from "@reduxjs/toolkit";
// createSlice 同时生成 action creator 与 reducer
const counterSlice = createSlice({
name: "counter",
initialState: { count: 0, status: "idle" },
reducers: {
increment(state) { state.count += 1; }, // 可"直接改",immer 保证不可变
incrementBy(state, action) { state.count += action.payload; },
},
});
export const { increment, incrementBy } = counterSlice.actions;
// configureStore:自动组合 reducer、内置 thunk 与 devtools
const store = configureStore({
reducer: { counter: counterSlice.reducer },
});
store.dispatch(increment());
console.log(store.getState().counter.count); // 1| 对比 | 原生 Redux | Redux Toolkit |
|---|---|---|
| 样板代码 | 多(action/reducer 手动写) | 少(createSlice 一键生成) |
| 不可变更新 | 手写展开语法 | immer 内部处理 |
| 异步 | 自行配中间件 | 内置 thunk |
| 调试工具 | 手动集成 | 自动集成 |
六、Zustand
极简 API,无 Provider 包裹,hooks 风格读取,体积仅约 1KB:
javascript
import { create } from "zustand";
const useStore = create((set) => ({
count: 0,
users: [],
increment: () => set((s) => ({ count: s.count + 1 })),
// 异步 action:直接 await,再 set
fetchUsers: async () => {
const res = await fetch("/api/users");
const users = await res.json();
set({ users });
},
}));
// 组件里用选择器精确订阅,避免多余渲染
function Counter() {
const count = useStore((s) => s.count); // 只订阅 count
const increment = useStore((s) => s.increment);
return <button onClick={increment}>{count}</button>;
}
// 组件外(非 React 环境)也能读写
useStore.getState().increment();七、Pinia
Vue 3 官方状态库,同时支持 Options 风格与 setup 风格:
javascript
import { defineStore } from "pinia";
import { ref, computed } from "vue";
// setup 风格:像写 Composition API
export const useCartStore = defineStore("cart", () => {
const items = ref([]);
const total = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.qty, 0)
);
function add(item) {
const found = items.value.find((x) => x.id === item.id);
if (found) found.qty++;
else items.value.push({ ...item, qty: 1 });
}
async function sync() { /* 异步同样直接 await */ }
return { items, total, add, sync };
});vue
<script setup>
import { useCartStore } from "@/stores/cart";
const cart = useCartStore(); // 拿到 store 实例,自动响应式
cart.add({ id: 1, name: "咖啡", price: 18 });
console.log(cart.total);
</script>持久化(官方插件 pinia-plugin-persistedstate):
javascript
export const useUserStore = defineStore("user", {
state: () => ({ token: "", name: "" }),
persist: true, // 自动同步到 localStorage
});八、三库对比
| 对比项 | Redux Toolkit | Zustand | Pinia |
|---|---|---|---|
| 绑定框架 | React(官方) | React 为主,可通用 | Vue 3 |
| 体积 | 较大(约 11KB) | 极小(约 1KB) | 小(约 2KB) |
| TypeScript | 支持好,样板多 | 优秀,推导强 | 优秀 |
| 中间件/插件 | 丰富(saga、persist…) | 中间件机制 | 插件机制 |
| 学习成本 | 高(概念多) | 低 | 低 |
| 调试 | Redux DevTools 强大 | DevTools 支持 | DevTools 支持 |
| 样板代码 | 中(createSlice 后已少) | 少 | 少 |
选型建议:React 大项目用 Redux Toolkit(生态成熟、可预测性强);轻量项目 Zustand;Vue 3 项目直接用 Pinia。
九、服务端状态:TanStack Query
服务端数据(接口结果)不适合放进 Redux——它是缓存而不是"状态",需要处理加载中、失败、过期、重试:
javascript
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
function UserList() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ["users"], // 缓存键,参数变自动重新请求
queryFn: () => fetch("/api/users").then((r) => r.json()),
staleTime: 60_000, // 一分钟内不重复请求
});
if (isLoading) return <div>加载中…</div>;
if (error) return <button onClick={refetch}>重试</button>;
return <ul>{data.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
// 变更:提交成功后让相关缓存失效,自动重新拉取
const client = useQueryClient();
const mutation = useMutation({
mutationFn: (name) => fetch("/api/users", { method: "POST", body: JSON.stringify({ name }) }),
onSuccess: () => client.invalidateQueries({ queryKey: ["users"] }),
});| 维度 | 全局状态库 | TanStack Query |
|---|---|---|
| 数据来源 | 客户端产生 | 服务端接口 |
| 生命周期 | 手动管理 | 自动(缓存/失效/重试) |
| 并发 | 手动处理 | 自动去重(请求合并) |
| 最佳实践 | 配合 Query 使用 | 单独使用 |
十、状态设计原则
- 能局部就不全局:只被一个组件用到的状态留在组件内。
- 派生状态不要存:能算出来的(如筛选结果、总和)用 computed/selector,不复制一份。
- 减少冗余:同一份数据只存一份,其余组件订阅。
- 状态扁平化:避免深层嵌套对象,难更新难维护。
- 明确 owner:一份状态只有一个"拥有者",其他都算派生。
- 服务端数据交给缓存层:不要手动塞进 store 管理 loading/error。
javascript
// 反例:把派生数据也存起来,改来源时容易忘更新副本
const [list, setList] = useState([]);
const [filtered, setFiltered] = useState([]);
// 更新 list 后忘记同步 filtered → 界面不一致
// 正例:只存来源,派生用计算
const filtered = useMemo(() => list.filter((x) => x.ok), [list]);十一、要点速查
| 概念 | 一句话记忆 |
|---|---|
| 单向数据流 | dispatch → reducer → store → 视图 |
| 纯函数 reducer | 同输入同输出,不修改原状态 |
| 不可变更新 | 返回新对象,引用变化触发更新 |
| 中间件 | 拦截 dispatch 做异步与日志 |
| Redux Toolkit | 官方现代写法,immer 简化不可变更新 |
| Zustand | 极简 hooks 状态库,选择器订阅 |
| Pinia | Vue 3 官方库,setup 风格 |
| TanStack Query | 服务端数据缓存与失效管理 |