状态管理是每个重要 React 应用程序的核心。如果做得对,你的代码库就能优雅地扩展;如果出错,你最终会陷入道具钻探的噩梦、陈旧的 UI 和组件重新渲染而被遗忘。好消息是 React 生态系统已经显着成熟。您现在拥有丰富的专用工具菜单 - Redux Toolkit、Zustand、Jotai、TanStack Query 和内置 Context API - 每个工具都针对特定类别的问题进行了优化。
本文深入探讨了每个解决方案,通过具体示例解释了权衡,并为您提供了一个可以立即应用于您自己的项目的决策框架。
了解不同类型的状态
在使用任何库之前,有必要准确了解您实际管理的状态类型。合并不同的状态类别是大多数过度设计的 React 应用程序的根本原因。
- 本地用户界面状态 — 下拉列表是否打开、哪个选项卡处于活动状态、受控输入的当前值。该状态由单个组件或小子树拥有。
- 共享客户端状态 — 多个可能遥远的组件需要读取或写入的数据:当前经过身份验证的用户、主题首选项、购物车。
- 服务器状态 — 源自服务器的数据本质上是异步的,需要缓存失效、后台重新获取和加载/错误生命周期管理。
- URL状态 — 过滤器、分页和搜索词应在页面刷新后继续存在并可通过链接共享。
- 表单状态 — 受控输入、验证错误和提交状态,通常最好由专用库(例如 React Hook Form)处理。
您能做的最有影响力的一件事就是抵制将每种状态类别倒入一家全球商店的冲动。每个类别都有不同的一致性要求、不同的生命周期和不同的更新频率。
React 上下文:内置选项
Context API 随 React 一起提供,不需要额外的依赖项。它适用于不经常更改且被许多组件消耗的状态——典型的例子是主题、区域设置和经过身份验证的用户对象。
// AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
interface AuthState {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthState | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (credentials: Credentials) => {
const user = await authService.login(credentials);
setUser(user);
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used inside AuthProvider');
return ctx;
}
上下文性能陷阱
上下文有一个众所周知的性能特征,这让许多开发人员感到困惑:每当上下文值引用发生变化时,每个消费者都会重新渲染。如果您在单个上下文中存储大型且频繁变化的对象,则会在整个树上触发不必要的重新渲染。
缓解策略是:
- 按更新频率分割上下文。将用户对象保留在一个上下文中,将 UI 首选项保留在另一上下文中。
- 记住值对象
useMemo因此,只有当数据实际发生变化时,引用才会发生变化。 - 使用
React.memo当消费者组件关心的道具没有改变时,他们会跳过重新渲染。
诚实的结论是,Context 对于低频全局状态非常出色,但它不是通用的状态管理解决方案。一旦您发现自己正在编写复杂的记忆来解决 Context 的重新渲染行为,那么就该使用专用的库了。
Redux 工具包:成熟的企业选择
Redux 因样板文件而闻名——这是它在 Toolkit 时代之前赢得的声誉。 Redux Toolkit (RTK) 消除了仪式,同时保留了 Redux 的强大之处:具有严格单向数据流的单一、可预测、可检查的状态树。
RTK船舶 createSlice,它从单个定义生成动作创建者和减速器,并在底层使用 Immer,这样你就可以编写看起来命令式但实际上是不可变应用的突变。
// features/cart/cartSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CartItem {
id: string;
name: string;
quantity: number;
price: number;
}
interface CartState {
items: CartItem[];
coupon: string | null;
}
const initialState: CartState = { items: [], coupon: null };
export const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem(state, action: PayloadAction<CartItem>) {
const existing = state.items.find(i => i.id === action.payload.id);
if (existing) {
existing.quantity += action.payload.quantity;
} else {
state.items.push(action.payload);
}
},
removeItem(state, action: PayloadAction<string>) {
state.items = state.items.filter(i => i.id !== action.payload);
},
applyCoupon(state, action: PayloadAction<string>) {
state.coupon = action.payload;
},
},
});
export const { addItem, removeItem, applyCoupon } = cartSlice.actions;
export default cartSlice.reducer;
RTK 查询:Redux 内的服务器状态
RTK 附带了一个名为 RTK Query 的配套产品,它直接在 Redux 存储中处理服务器状态。它从端点定义自动生成 React 挂钩并管理缓存、失效和乐观更新。
// services/productsApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Product'],
endpoints: (builder) => ({
getProducts: builder.query<Product[], void>({
query: () => '/products',
providesTags: ['Product'],
}),
updateProduct: builder.mutation<Product, Partial<Product>>({
query: ({ id, ...patch }) => ({
url: `/products/${id}`,
method: 'PATCH',
body: patch,
}),
invalidatesTags: ['Product'],
}),
}),
});
export const { useGetProductsQuery, useUpdateProductMutation } = productsApi;
当您需要通过 Redux DevTools 进行时间旅行调试时,当多个团队需要通过强制约定为共享状态模型做出贡献时,或者当您拥有受益于中间件(例如)的复杂跨切片业务逻辑时,Redux Toolkit 是正确的选择 redux-saga 或 redux-observable.
Zustand:无需仪式的轻量级全局状态
Zustand采取了一种完全不同的方法。没有提供者,没有减速器,也没有操作类型。您将存储定义为具有状态和方法的纯 JavaScript 对象,然后使用单个钩子使用它。整个 API 适合在一个屏幕上。
// stores/useCartStore.ts
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
totalPrice: () => number;
clear: () => void;
}
export const useCartStore = create<CartStore>()(
devtools(
persist(
(set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id
? { ...i, quantity: i.quantity + item.quantity }
: i
),
};
}
return { items: [...state.items, item] };
}),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
totalPrice: () =>
get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
clear: () => set({ items: [] }),
}),
{ name: 'cart-storage' }
)
)
);
精细订阅和性能
Zustand通过基于选择器的订阅解决了Context的重新渲染问题。仅当组件选择的状态切片发生更改时,组件才会重新渲染。
// Only re-renders when items.length changes, not on price changes
const itemCount = useCartStore((state) => state.items.length);
// Only re-renders when the total changes
const total = useCartStore((state) => state.totalPrice());
Zustand 的中间件生态系统涵盖持久性 localStorage、DevTools 集成、Immer 式突变和 URL 同步。对于想要 Redux 级别的功能而无需 Redux 级别的设置成本的团队来说,它是首选。
Jotai:受反冲启发的原子态
Jotai 将状态模型描述为一张原子图——可以相互派生的微小的、可组合的状态单元。该模型对于动态状态特别强大:想象一个编辑器节点列表,其中每个节点都有自己独立的选择/焦点状态,或者一个复杂的形式,其中字段可见性取决于其他字段值。
// atoms/filterAtoms.ts
import { atom, selector } from 'jotai';
export const searchTermAtom = atom('');
export const categoryAtom = atom<string | null>(null);
export const productsAtom = atom<Product[]>([]);
// Derived atom — recomputes only when its dependencies change
export const filteredProductsAtom = atom((get) => {
const products = get(productsAtom);
const term = get(searchTermAtom).toLowerCase();
const category = get(categoryAtom);
return products.filter((p) => {
const matchesTerm = p.name.toLowerCase().includes(term);
const matchesCategory = category === null || p.category === category;
return matchesTerm && matchesCategory;
});
});
// ProductList.tsx
import { useAtom, useAtomValue } from 'jotai';
function ProductList() {
const [searchTerm, setSearchTerm] = useAtom(searchTermAtom);
const filtered = useAtomValue(filteredProductsAtom);
return (
<>
<input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />
{filtered.map((p) => <ProductCard key={p.id} product={p} />)}
</>
);
}
Jotai 的细粒度反应性模型意味着当搜索词发生变化时,只有真正订阅的组件 filteredProductsAtom 重新渲染。仅订阅的组件 categoryAtom 未受影响。这使得 Jotai 非常适合具有密集、相互依赖的状态图的应用程序。
TanStack 查询:服务器状态的正确归宿
TanStack Query(以前称为 React Query)不是一个通用的状态管理库 - 它是一个服务器状态库,并且它比生态系统中的其他任何库都更好地处理该责任。在不使用 TanStack 查询的情况下在 React 中获取、缓存、同步和更新远程数据通常意味着在本地状态或全局存储中手动管理加载标志、错误对象和缓存失效。 TanStack Query 可自动执行所有这些操作。
// hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export function useProducts(filters: ProductFilters) {
return useQuery({
queryKey: ['products', filters],
queryFn: () => api.getProducts(filters),
staleTime: 5 * 60 * 1000, // treat data as fresh for 5 minutes
gcTime: 10 * 60 * 1000, // keep unused data in cache for 10 minutes
});
}
export function useUpdateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (update: ProductUpdate) => api.updateProduct(update),
// Optimistic update
onMutate: async (update) => {
await queryClient.cancelQueries({ queryKey: ['products'] });
const previous = queryClient.getQueryData(['products']);
queryClient.setQueryData(['products'], (old: Product[]) =>
old.map((p) => (p.id === update.id ? { ...p, ...update } : p))
);
return { previous };
},
onError: (_err, _update, context) => {
queryClient.setQueryData(['products'], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}
查询键数组是 TanStack Query 的缓存原语。共享相同密钥的任何查询共享相同的缓存条目。当您使某个密钥无效时,订阅该密钥的每个组件都会在后台自动重新获取,并在新数据到达时进行更新。
Next.js 中的预取和水化
在 Next.js 应用程序中,您可以在服务器上预取查询并将其脱水到 HTML 有效负载中,从而完全消除初始页面渲染的加载旋转器。
// app/products/page.tsx (Next.js App Router)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
export default async function ProductsPage() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['products', {}],
queryFn: () => api.getProducts({}),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ProductList />
</HydrationBoundary>
);
}
组合库:实用的架构
真正的应用程序几乎总是受益于组合多个工具,每个工具负责自己的状态类别。结构良好的中型到大型 React 应用程序可能如下所示:
- TanStack查询 拥有所有服务器状态。它获取、缓存和同步远程数据。没有其他东西会影响 API 响应。
- 祖斯坦 拥有全局客户端状态——购物车、经过身份验证的用户会话、UI 首选项以及任何对于 Context 而言过于复杂的跨功能协调。
- React 背景 拥有低流失配置——当前主题、区域设置、功能标志。
- useState / useReducer 自己的本地组件状态 - 模式打开/关闭、表单字段值(或用于复杂表单的 React Hook Form)、选项卡选择。
- URL 搜索参数 自己的导航状态 - 活动过滤器、排序顺序、当前页码。
这种关注点的分离不仅仅是美观的。这意味着您的服务器状态缓存永远不会被 UI 状态污染,您的全局客户端存储永远不会因 TanStack 查询会更有效地缓存的响应数据而膨胀,并且您的 Context 永远不会触发应用程序范围的重新渲染,因为有人在其中存储了快速变化的值。
值得了解的性能模式
具有重新选择功能的记忆选择器
从 Redux 存储中派生计算值时,使用 Reselect 以避免在每次渲染时重新计算。 RTK 再导出 createSelector 从重新选择。
import { createSelector } from '@reduxjs/toolkit';
const selectItems = (state: RootState) => state.cart.items;
export const selectCartSummary = createSelector(selectItems, (items) => ({
count: items.reduce((n, i) => n + i.quantity, 0),
total: items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}));
使用 useTransition 分割渲染
React 18 颗 useTransition 允许您将状态更新标记为非紧急,以便浏览器可以中断它以处理更高优先级的工作(例如用户输入)。当状态更改触发昂贵的重新渲染时,这特别有用。
const [isPending, startTransition] = useTransition();
const handleFilterChange = (value: string) => {
startTransition(() => {
setFilterValue(value); // expensive downstream re-render is deferrable
});
};
避免对象身份问题
幻像重新渲染的一个常见来源是在渲染内创建新的对象或数组文字。 useMemo 和 useCallback 是你的工具,但只有当你有性能问题的证据时才应用它们——过早的记忆会增加认知开销,但不会保证好处。
决策框架
为新功能选择状态管理方法时,请使用以下决策树:
- 状态是纯粹本地于一个组件还是一棵小子树?使用
useState或useReducer. - 状态是否表示从服务器获取的数据?使用 TanStack查询.
- 树上的许多组件都需要全局客户端状态吗?伸手去拿 祖斯坦 除非您已经在 Redux 代码库中。
- 状态是否很少变化并服务于配置目的(主题、区域设置)?使用 React 背景.
- 国家是否与许多派生价值观高度关联?考虑 佐泰.
- 您是否需要时间旅行调试、严格的架构约定或复杂的异步传奇?使用 Redux 工具包.
要避免的常见反模式
- 在 Redux 或 Zustand 中存储服务器响应。 让 TanStack Query 拥有该数据。在全局存储中复制它会产生同步错误。
- 将所有东西集中在一个大型商店中。 属于同一切片的不相关状态会强制不必要的耦合并使测试变得更加困难。
- 使用 Context 进行高频更新。 每个上下文值的更改都会重新呈现所有消费者。对于每次击键或动画帧都会发生变化的值,Context 是错误的工具。
- 派生组件内部的状态。 取决于状态的计算值应该是记忆选择器或派生原子,而不是在每个渲染上运行的内联计算。
- 忽略加载和错误状态。 服务器状态本质上是异步的。 TanStack 查询表面
isLoading,isError, 和error对于每个查询;使用它们。
结论
React 中的状态管理环境从未如此健康。您不再需要在沉重的框架和从头开始构建一切之间进行选择。 Redux Toolkit 提供企业级可预测性,无需其前身的样板。 Zustand 只需一小部分设置即可实现相同的功能。 Jotai 解决了细粒度、动态状态图中的难题。 TanStack Query 已经明确解决了服务器状态问题 — 如果您仍在管理 API 响应 useEffect 和 useState,你有责任移民。
最重要的洞察是分类的:首先确定您正在处理的状态类型,然后选择针对该类别优化的工具。抵制将所有内容都集中到一个系统中的本能。分层方法——用于服务器状态的 TanStack 查询、用于全局客户端状态的 Zustand、用于配置的上下文以及用于其他所有内容的本地状态——生成的代码库更容易推理、更容易测试,并且随着团队和需求的增长而更易于维护。