状态管理方案
问题
React 有哪些状态管理方案?Redux、Context、Zustand、Jotai、Recoil、Valtio 各有什么特点?
答案
React 状态管理方案分为多个流派:
| 类型 | 代表 | 特点 |
|---|---|---|
| 原生方案 | Context + useReducer | React 内置,适合简单场景 |
| Flux 架构 | Redux | 单向数据流,严格规范 |
| 原子化 | Jotai、Recoil | 自底向上,细粒度更新 |
| 代理模式 | Zustand、Valtio | 简洁 API,易于使用 |
React Context
基本用法
import { createContext, useContext, useState, ReactNode } from 'react';
// 1. 创建 Context
interface ThemeContextValue {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
// 2. 创建 Provider
function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. 使用 Context
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// 4. 消费组件
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{ background: theme === 'dark' ? '#333' : '#fff' }}
>
Toggle Theme
</button>
);
}
Context 的性能问题
// ❌ 所有消费者都会重渲染
const AppContext = createContext({ user: null, theme: 'light', locale: 'en' });
function App() {
const [state, setState] = useState({ user: null, theme: 'light', locale: 'en' });
return (
<AppContext.Provider value={state}>
<UserInfo /> {/* user 变化时重渲染 */}
<ThemeSwitch /> {/* user 变化时也重渲染! */}
<LocalePicker />{/* user 变化时也重渲染! */}
</AppContext.Provider>
);
}
优化方案
方案 1:拆分 Context
// ✅ 拆分成多个 Context
const UserContext = createContext<User | null>(null);
const ThemeContext = createContext<Theme>('light');
const LocaleContext = createContext<Locale>('en');
function App() {
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<LocaleContext.Provider value={locale}>
<Content />
</LocaleContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
方案 2:分离数据和操作
// ✅ 数据和操作分离
const StateContext = createContext<State | null>(null);
const DispatchContext = createContext<Dispatch | null>(null);
function Provider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
// 只需要 dispatch 的组件不会因 state 变化重渲染
function AddButton() {
const dispatch = useContext(DispatchContext)!;
return <button onClick={() => dispatch({ type: 'ADD' })}>Add</button>;
}
Redux
核心概念
| 概念 | 说明 |
|---|---|
| Store | 单一数据源,存储应用状态 |
| Action | 描述发生了什么的普通对象 |
| Reducer | 纯函数,根据 action 返回新状态 |
| Dispatch | 发送 action 到 store |
| Selector | 从 state 中提取数据 |
Redux Toolkit 示例
// store/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CounterState {
value: number;
history: number[];
}
const initialState: CounterState = {
value: 0,
history: [],
};
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.value += 1;
state.history.push(state.value);
},
decrement(state) {
state.value -= 1;
state.history.push(state.value);
},
incrementByAmount(state, action: PayloadAction<number>) {
state.value += action.payload;
state.history.push(state.value);
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// hooks.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// Counter.tsx
import { useAppDispatch, useAppSelector } from './hooks';
import { increment, decrement, incrementByAmount } from './store/counterSlice';
function Counter() {
const count = useAppSelector(state => state.counter.value);
const dispatch = useAppDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
);
}
Redux 异步操作
// 使用 createAsyncThunk
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
interface User {
id: number;
name: string;
}
interface UserState {
user: User | null;
loading: boolean;
error: string | null;
}
export const fetchUser = createAsyncThunk(
'user/fetch',
async (userId: number) => {
const response = await fetch(`/api/users/${userId}`);
return response.json() as Promise<User>;
}
);
const userSlice = createSlice({
name: 'user',
initialState: { user: null, loading: false, error: null } as UserState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message ?? 'Failed to fetch';
});
},
});
Zustand
轻量级状态管理,API 简洁:
import { create } from 'zustand';
// 定义 store
interface BearState {
bears: number;
increase: () => void;
decrease: () => void;
reset: () => void;
}
const useBearStore = create<BearState>((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
decrease: () => set((state) => ({ bears: state.bears - 1 })),
reset: () => set({ bears: 0 }),
}));
// 使用
function BearCounter() {
const bears = useBearStore((state) => state.bears);
return <h1>{bears} bears</h1>;
}
function Controls() {
const increase = useBearStore((state) => state.increase);
return <button onClick={increase}>Add Bear</button>;
}
Zustand 中间件
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
const useStore = create<State>()(
devtools(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{ name: 'counter-storage' } // localStorage key
)
)
);
Zustand 异步操作
interface UserStore {
user: User | null;
loading: boolean;
fetchUser: (id: number) => Promise<void>;
}
const useUserStore = create<UserStore>((set) => ({
user: null,
loading: false,
fetchUser: async (id) => {
set({ loading: true });
try {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
set({ user, loading: false });
} catch {
set({ loading: false });
}
},
}));
Jotai
原子化状态管理,自底向上:
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
// 定义原子
const countAtom = atom(0);
const doubleCountAtom = atom((get) => get(countAtom) * 2);
// 可写派生原子
const incrementAtom = atom(
null, // 只写,不需要读
(get, set) => set(countAtom, get(countAtom) + 1)
);
// 使用
function Counter() {
const [count, setCount] = useAtom(countAtom);
const double = useAtomValue(doubleCountAtom);
const increment = useSetAtom(incrementAtom);
return (
<div>
<p>Count: {count}</p>
<p>Double: {double}</p>
<button onClick={increment}>+</button>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
Jotai 异步原子
// 异步原子
const userAtom = atom(async () => {
const response = await fetch('/api/user');
return response.json();
});
// 使用 Suspense
function UserInfo() {
const user = useAtomValue(userAtom); // 会抛出 Promise
return <div>{user.name}</div>;
}
function App() {
return (
<Suspense fallback={<Loading />}>
<UserInfo />
</Suspense>
);
}
Jotai 组合原子
const firstNameAtom = atom('John');
const lastNameAtom = atom('Doe');
// 派生原子
const fullNameAtom = atom(
(get) => `${get(firstNameAtom)} ${get(lastNameAtom)}`,
(get, set, newName: string) => {
const [first, last] = newName.split(' ');
set(firstNameAtom, first);
set(lastNameAtom, last ?? '');
}
);
Recoil
Facebook 出品的原子化状态管理:
import { atom, selector, useRecoilState, useRecoilValue, RecoilRoot } from 'recoil';
// 原子
const todoListState = atom<Todo[]>({
key: 'todoListState',
default: [],
});
// 选择器(派生状态)
const filteredTodoListState = selector({
key: 'filteredTodoListState',
get: ({ get }) => {
const filter = get(todoListFilterState);
const list = get(todoListState);
switch (filter) {
case 'completed':
return list.filter(item => item.isComplete);
case 'uncompleted':
return list.filter(item => !item.isComplete);
default:
return list;
}
},
});
// 使用
function TodoList() {
const todoList = useRecoilValue(filteredTodoListState);
return (
<ul>
{todoList.map(todo => (
<TodoItem key={todo.id} item={todo} />
))}
</ul>
);
}
// 根组件需要包裹 RecoilRoot
function App() {
return (
<RecoilRoot>
<TodoList />
</RecoilRoot>
);
}
Recoil 异步选择器
const userQuery = selector({
key: 'userQuery',
get: async ({ get }) => {
const userId = get(currentUserIdState);
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
});
// 自动支持 Suspense
function UserInfo() {
const user = useRecoilValue(userQuery);
return <div>{user.name}</div>;
}
Valtio
基于代理的响应式状态:
import { proxy, useSnapshot } from 'valtio';
// 创建代理状态
const state = proxy({
count: 0,
user: { name: 'Alice' },
});
// 直接修改(可变式 API)
const increment = () => {
state.count++;
};
const updateName = (name: string) => {
state.user.name = name;
};
// 组件中使用
function Counter() {
const snap = useSnapshot(state);
return (
<div>
<p>Count: {snap.count}</p>
<p>User: {snap.user.name}</p>
<button onClick={increment}>+</button>
</div>
);
}
Valtio 派生状态
import { proxy, derive } from 'valtio/utils';
const state = proxy({
firstName: 'John',
lastName: 'Doe',
});
// 派生状态
const derived = derive({
fullName: (get) => `${get(state).firstName} ${get(state).lastName}`,
});
// 订阅变化
import { subscribe } from 'valtio';
subscribe(state, () => {
console.log('state changed:', state);
});
方案对比
| 特性 | Redux | Zustand | Jotai | Recoil | Valtio | Context |
|---|---|---|---|---|---|---|
| 包大小 | ~12KB | ~1KB | ~3KB | ~20KB | ~3KB | 0 |
| 学习曲线 | 陡峭 | 平缓 | 平缓 | 中等 | 平缓 | 平缓 |
| 模式 | Flux | 单Store | 原子 | 原子 | 代理 | 原生 |
| DevTools | ✅ | ✅ | ✅ | ✅ | ✅ | 有限 |
| 异步支持 | 中间件 | 内置 | 内置 | 内置 | 内置 | 手动 |
| SSR | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ |
| TypeScript | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 适合场景 | 大型应用 | 中小型 | 细粒度 | 细粒度 | 中小型 | 简单场景 |
常见面试问题
Q1: Redux 的三大原则是什么?
答案:
- 单一数据源:整个应用状态存储在一个 store 中
- 状态只读:只能通过 dispatch action 修改状态,不能直接修改
- 纯函数修改:Reducer 是纯函数,相同输入产生相同输出
// 单一数据源
const store = configureStore({ reducer: rootReducer });
// 状态只读 - 通过 action 修改
dispatch({ type: 'INCREMENT' }); // ✅
// state.count++; // ❌
// 纯函数
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 }; // 返回新对象
default:
return state;
}
}
Q2: Zustand 和 Redux 的区别?
答案:
| 特性 | Redux | Zustand |
|---|---|---|
| 包大小 | ~12KB | ~1KB |
| 样板代码 | 多(action, reducer, dispatch) | 少(一个 create) |
| Provider | 需要 <Provider> | 不需要 |
| 中间件 | 复杂配置 | 简单组合 |
| 异步 | 需要 thunk/saga | 直接 async |
| 调试 | Redux DevTools | 同样支持 |
// Redux
const store = configureStore({ reducer });
dispatch(increment());
// Zustand
const useStore = create((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}));
const increment = useStore(s => s.increment);
increment();
Q3: 什么是原子化状态管理?Jotai 和 Recoil 有什么区别?
答案:
原子化状态管理:将状态分割成独立的原子,每个原子只触发订阅它的组件更新。
| 特性 | Jotai | Recoil |
|---|---|---|
| 创建方式 | atom(initialValue) | atom({ key, default }) |
| Key 要求 | 不需要 | 必须唯一 |
| 包大小 | ~3KB | ~20KB |
| API 风格 | 更简洁 | 更完整 |
| 维护方 | 社区 |
// Jotai - 无需 key
const countAtom = atom(0);
// Recoil - 需要 key
const countState = atom({
key: 'countState',
default: 0,
});
Q4: 如何选择状态管理方案?
答案:
| 场景 | 推荐方案 |
|---|---|
| 小型应用、简单全局状态 | Context |
| 需要严格规范、大团队 | Redux |
| 中小型应用、追求简洁 | Zustand |
| 细粒度更新、原子化 | Jotai |
| 已有 Redux 经验、需要原子化 | Recoil |
| 喜欢可变式 API | Valtio |
Q5: Context 的性能问题如何解决?
答案:
问题:Provider value 变化时,所有消费者都会重渲染。
解决方案:
- 拆分 Context:不同数据使用不同 Context
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
</UserContext.Provider>
- Memo + 分离数据和操作
const StateContext = createContext(state);
const ActionsContext = createContext(actions);
// actions 用 useMemo 稳定引用
const actions = useMemo(() => ({
increment: () => dispatch({ type: 'INC' }),
}), []);
- 使用状态管理库
// Zustand 自动处理精细更新
const count = useStore(s => s.count); // 只订阅 count
Q6: Zustand 为什么越来越流行?它和 Redux 的核心区别?
答案:
Zustand 近年来增长迅猛,npm 周下载量已接近 Redux Toolkit,主要原因是它在开发体验和性能上都做得更好。
Zustand 流行的核心原因:
| 维度 | Redux(含 RTK) | Zustand |
|---|---|---|
| 样板代码 | 多(slice、action、reducer、store、Provider) | 极少(一个 create 搞定) |
| Provider | 必须用 <Provider> 包裹根组件 | 不需要 Provider |
| 包大小 | ~12KB(RTK) | ~1KB |
| 学习成本 | Flux 架构、中间件、RTK API | 几乎为零,5 分钟上手 |
| 异步处理 | 需要 createAsyncThunk 或中间件 | 直接在 action 里写 async |
| 精确更新 | 需要 useSelector + shallowEqual | 内置 selector,默认精确更新 |
| React 外使用 | 需要 store.getState() | 原生支持组件外访问 |
| TypeScript | 类型推导复杂(RootState、AppDispatch) | 自动推导,几乎零配置 |
代码对比——实现同一个功能:
// 1. 定义 slice
import { createSlice, configureStore, PayloadAction } from '@reduxjs/toolkit';
import { useSelector, useDispatch, Provider } from 'react-redux';
interface CartState {
items: CartItem[];
total: number;
}
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], total: 0 } as CartState,
reducers: {
addItem(state, action: PayloadAction<CartItem>) {
state.items.push(action.payload);
state.total += action.payload.price;
},
removeItem(state, action: PayloadAction<number>) {
const index = state.items.findIndex(i => i.id === action.payload);
if (index !== -1) {
state.total -= state.items[index].price;
state.items.splice(index, 1);
}
},
},
});
// 2. 配置 store
const store = configureStore({ reducer: { cart: cartSlice.reducer } });
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
// 3. 类型化 hooks
const useAppSelector = <T,>(selector: (state: RootState) => T) => useSelector(selector);
const useAppDispatch = () => useDispatch<AppDispatch>();
// 4. 使用(需要 Provider 包裹)
function CartCount() {
const total = useAppSelector(s => s.cart.total);
return <span>总价:{total}</span>;
}
function App() {
return (
<Provider store={store}>
<CartCount />
</Provider>
);
}
import { create } from 'zustand';
// 一个 create 搞定一切
interface CartStore {
items: CartItem[];
total: number;
addItem: (item: CartItem) => void;
removeItem: (id: number) => void;
}
const useCartStore = create<CartStore>((set) => ({
items: [],
total: 0,
addItem: (item) => set((s) => ({
items: [...s.items, item],
total: s.total + item.price,
})),
removeItem: (id) => set((s) => {
const item = s.items.find(i => i.id === id);
return {
items: s.items.filter(i => i.id !== id),
total: s.total - (item?.price ?? 0),
};
}),
}));
// 直接使用,无需 Provider
function CartCount() {
const total = useCartStore((s) => s.total);
return <span>总价:{total}</span>;
}
核心区别总结:
- 大型团队:Redux 的严格规范(action → reducer 单向流)有利于代码审查和协作
- 复杂中间件需求:日志、撤销重做、离线同步等复杂中间件生态 Redux 更成熟
- 已有大量 Redux 代码:迁移成本高,继续用 Redux 更合理
- 需要时间旅行调试:Redux DevTools 的时间旅行功能更完善
Q7: 什么时候应该用 Context,什么时候用状态管理库?
答案:
这是一个非常高频的面试问题。核心判断标准是更新频率和消费者数量。
判断决策树:
Context 适用场景:
| 场景 | 原因 |
|---|---|
| 主题切换(light/dark) | 低频更新,几乎不变 |
| 用户认证信息 | 登录后基本不变 |
| 国际化语言 | 切换频率极低 |
| 路由信息 | React Router 内部就用 Context |
| 依赖注入(API client、配置) | 初始化后不变 |
// ✅ 主题 —— 低频更新
const ThemeContext = createContext<'light' | 'dark'>('light');
// ✅ 认证 —— 登录后基本不变
const AuthContext = createContext<AuthState | null>(null);
// ✅ 配置注入 —— 初始化后不变
const ConfigContext = createContext<AppConfig>(defaultConfig);
状态管理库适用场景:
| 场景 | 原因 |
|---|---|
| 购物车 | 频繁增删,多组件消费 |
| 表单联动 | 字段间相互影响,更新频繁 |
| 实时数据(股票、聊天) | 高频更新,需精确订阅 |
| 复杂筛选/排序 | 多维度状态交叉影响 |
| 跨页面共享 | 多个路由页面共享同一状态 |
// ✅ 购物车 —— 高频更新、多组件消费
const useCartStore = create<CartStore>((set) => ({
items: [],
addItem: (item: CartItem) => set((s) => ({ items: [...s.items, item] })),
removeItem: (id: number) => set((s) => ({
items: s.items.filter(i => i.id !== id),
})),
get totalPrice() {
return this.items.reduce((sum, i) => sum + i.price, 0);
},
}));
// ✅ 实时聊天 —— 高频更新、精确订阅
const useChatStore = create<ChatStore>((set) => ({
messages: [],
unreadCount: 0,
addMessage: (msg: Message) => set((s) => ({
messages: [...s.messages, msg],
unreadCount: s.unreadCount + 1,
})),
}));
Context vs 状态管理库对比:
| 维度 | Context | 状态管理库(Zustand 等) |
|---|---|---|
| 精确订阅 | 不支持,value 变就全更新 | 支持 selector 精确订阅 |
| 更新性能 | 所有消费者重渲染 | 只有订阅了变化字段的组件重渲染 |
| 嵌套问题 | 多 Context 容易嵌套地狱 | 无需 Provider |
| React 外使用 | 不支持 | 支持(工具函数、中间件中) |
| DevTools | 基本没有 | 完善的调试工具 |
| 额外依赖 | 零依赖 | 需要安装包 |
| 学习成本 | React 原生 API | 需学习库的 API |
"Context 是 React 的状态管理方案" —— 这个说法不准确。Context 本质是依赖注入机制,它解决的是"如何把值传递给深层组件"的问题,而不是"如何高效管理和更新状态"。Context 没有精确订阅、没有中间件、没有 DevTools,不应该把它当作状态管理库来用。
Q8: 服务端状态和客户端状态有什么区别?React Query/SWR 解决什么问题?
答案:
这是现代 React 开发中非常重要的概念区分。传统做法把所有状态都放进 Redux/Zustand 管理,但实际上服务端状态和客户端状态的特性完全不同,应该用不同的工具管理。
两种状态的本质区别:
| 维度 | 客户端状态(Client State) | 服务端状态(Server State) |
|---|---|---|
| 数据来源 | 用户交互产生 | 远程 API 获取 |
| 所有权 | 前端完全控制 | 后端拥有,前端只有"快照" |
| 是否可能过期 | 不会 | 会过期(别人可能修改了) |
| 同步需求 | 无 | 需要与后端保持同步 |
| 示例 | 主题、表单输入、UI 状态、模态框 | 用户列表、商品数据、评论 |
传统方案的痛点:
// ❌ 需要手动处理大量状态
interface UserState {
data: User[] | null;
loading: boolean;
error: string | null;
// 还缺少:缓存、过期、重试、分页、乐观更新...
}
const fetchUsers = createAsyncThunk('users/fetch', async () => {
const res = await fetch('/api/users');
return res.json();
});
// 每个接口都要写这一套 pending/fulfilled/rejected
const userSlice = createSlice({
name: 'users',
initialState: { data: null, loading: false, error: null } as UserState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => { state.loading = true; })
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message ?? null;
});
},
});
// 问题:缓存何时过期?标签页切回来要不要刷新?失败了自动重试吗?
React Query(TanStack Query)解决方案:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// ✅ 一行代码搞定:加载、缓存、过期、重试、去重
function UserList() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json()) as Promise<User[]>,
staleTime: 5 * 60 * 1000, // 5 分钟内认为数据是新鲜的
gcTime: 30 * 60 * 1000, // 30 分钟后回收缓存
retry: 3, // 失败自动重试 3 次
refetchOnWindowFocus: true, // 标签页切回来自动刷新
});
if (isLoading) return <Loading />;
if (error) return <Error message={error.message} />;
return (
<ul>
{data?.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
function AddUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newUser: CreateUserDTO) =>
fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser),
}).then(res => res.json()),
// 乐观更新:先更新 UI,再等服务端确认
onMutate: async (newUser) => {
await queryClient.cancelQueries({ queryKey: ['users'] });
const previous = queryClient.getQueryData<User[]>(['users']);
queryClient.setQueryData<User[]>(['users'], (old) => [
...(old ?? []),
{ ...newUser, id: Date.now() } as User,
]);
return { previous };
},
onError: (_err, _newUser, context) => {
// 失败时回滚
queryClient.setQueryData(['users'], context?.previous);
},
onSettled: () => {
// 无论成功失败,都重新获取最新数据
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
return (
<button onClick={() => mutation.mutate({ name: 'New User', email: 'new@test.com' })}>
{mutation.isPending ? '添加中...' : '添加用户'}
</button>
);
}
SWR 对比:
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(res => res.json());
function UserList() {
// SWR = Stale-While-Revalidate(先用缓存,后台刷新)
const { data, error, isLoading, mutate } = useSWR<User[]>(
'/api/users',
fetcher,
{
revalidateOnFocus: true,
dedupingInterval: 2000, // 2 秒内去重
}
);
if (isLoading) return <Loading />;
if (error) return <Error />;
return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
React Query vs SWR 对比:
| 特性 | React Query | SWR |
|---|---|---|
| 包大小 | ~13KB | ~4KB |
| Mutation 支持 | 完善(onMutate、乐观更新) | 基础 |
| DevTools | ✅ 专属 DevTools | ❌ |
| 离线支持 | ✅ 内置 | 需手动 |
| 分页/无限滚动 | useInfiniteQuery | useSWRInfinite |
| 缓存管理 | 精细(gcTime、staleTime) | 简单 |
| 适合场景 | 复杂数据交互(增删改查) | 简单数据读取为主 |
将状态分为两类分别管理,是目前社区的主流共识:
- 客户端状态 →
useState/Zustand(轻量、简单) - 服务端状态 →
React Query/SWR(缓存、同步、重试)
这样 Redux 式的"全局大 Store"就可以被拆解为更小、更专注的工具组合,代码量和维护成本都会大幅下降。