狀態管理是每個重要 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、用於配置的上下文以及用於其他所有內容的本地狀態——生成的程式碼庫更容易推理、更容易測試,並且隨著團隊和需求的增長而更易於維護。