Featured image of post 2024 年的 React 状态管理:选择正确的工具Featured image of post 2024 年的 React 状态管理:选择正确的工具

2024 年的 React 状态管理:选择正确的工具

将 2024 年的 React 状态管理选项:useState、useReducer、useContext、Zustand、Jotai 和 TanStack Query 与性能模式进行比较。

到 2024 年,React 的状态管理环境已经成熟,选择正确的工具只是了解状态类别而不是选择获胜者。核心库为本地问题提供 useStateuseReducer ,而生态系统为全局 UI 状态和服务器状态提供专门的解决方案。关键的见解是,不同类别的状态受益于不同的工具,适当地混合它们会产生最可维护的应用程序。

useState 和 useReducer:内置的简单性

对于组件本地状态,useState 仍然是最合适的默认值。它有效地处理表单输入、切换状态和简单的 UI 状态。当状态逻辑涉及多个子值或复杂的转换时,useReducer 就变得很有价值。

// Simple counter
const [count, setCount] = useState(0);

// Form state
const [form, setForm] = useState({ name: "", email: "" });
// Complex wizard state with useReducer
function wizardReducer(state, action) {
  switch (action.type) {
    case "NEXT_STEP":
      return { ...state, step: state.step + 1 };
    case "PREV_STEP":
      return { ...state, step: state.step - 1 };
    case "SET_FIELD":
      return {
        ...state,
        data: { ...state.data, [action.field]: action.value },
      };
    case "RESET":
      return initialState;
    default:
      return state;
  }
}
标准使用状态使用Reducer
复杂性简单的价值观多个子值
逻辑位置同地办公已提取,可测试
依赖关系独立链接转换
样板最小更多代码,更可预测

useContext:内置但有限

useContext 解决了支柱钻井问题,但存在性能限制。任何上下文值更改都会导致所有使用者重新渲染,无论他们是否使用更改的部分。

// Problem: theme changes cause AuthConsumer to re-render
function App() {
  const [theme, setTheme] = useState("light");
  const [user, setUser] = useState(null);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <AuthContext.Provider value={{ user, setUser }}>
        <Layout />
      </AuthContext.Provider>
    </ThemeContext.Provider>
  );
}

解决方法是按更改频率分割上下文并在值上使用 useMemo

const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
const authValue = useMemo(() => ({ user, setUser }), [user]);

对于不经常更改的全局状态(主题、区域设置、身份验证状态),useContext 就足够了。对于高频更新,专用库可提供更好的性能。


Zustand:最小全局状态

Zustand 已成为领先的轻量级全球状态库。它的 API 在设计上是最小的:创建一个存储,通过钩子访问状态,然后直接变异。

import { create } from "zustand";

const useCartStore = create((set, get) => ({
  items: [],
  total: 0,
  addItem: (item) =>
    set((state) => ({
      items: [...state.items, item],
      total: state.total + item.price,
    })),
  removeItem: (id) =>
    set((state) => {
      const item = state.items.find((i) => i.id === id);
      return {
        items: state.items.filter((i) => i.id !== id),
        total: state.total - (item?.price || 0),
      };
    }),
}));

function CartSummary() {
  // Component only re-renders when 'total' changes
  const total = useCartStore((state) => state.total);
  return <div>Total: ${total}</div>;
}

主要优点:无提供程序包装、通过选择器相等自动渲染优化、简单的中间件(persist、devtools、immer)、TypeScript 优先设计以及约 1KB 的捆绑包大小。选择器确保组件仅在它们访问的切片上重新渲染。


Jotai:原子状态管理

Jotai 采用了不同的方法,在原子级别管理状态。每个状态都是一个独立的、可以组合的、惰性求值的原子。

import { atom, useAtom } from "jotai";

const searchQueryAtom = atom("");
const debouncedQueryAtom = atom((get) => {
  const query = get(searchQueryAtom);
  return query; // In practice, add debounce logic here
});
const resultsAtom = atom(async (get) => {
  const query = get(debouncedQueryAtom);
  if (!query) return [];
  const res = await fetch(`/api/search?q=${query}`);
  return res.json();
});

function Search() {
  const [query, setQuery] = useAtom(searchQueryAtom);
  const [results] = useAtom(resultsAtom);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ResultsList results={results} />
    </div>
  );
}

好处包括粒度重新渲染(只有更改的原子才会触发更新)、惰性求值(原子仅在使用时才计算)、基本情况没有全局提供程序以及树可摇动架构。 Jotai 在并发渲染场景中表现出色,因为每个原子都可以通过 Suspense 独立悬挂。


TanStack 查询:服务器状态

TanStack Query(以前称为 React Query)处理服务器状态:从后端获取并与后端同步的数据。服务器状态与 UI 状态有着根本的不同——它主要存在于服务器上,并且获取它是一个具有固有延迟的异步操作。

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

function ProductPage({ id }) {
  const queryClient = useQueryClient();

  const { data, isLoading, error } = useQuery({
    queryKey: ["product", id],
    queryFn: () => fetch(`/api/products/${id}`).then((r) => r.json()),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });

  const mutation = useMutation({
    mutationFn: (updated) =>
      fetch(`/api/products/${id}`, {
        method: "PATCH",
        body: JSON.stringify(updated),
      }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["product", id] });
    },
  });

  if (isLoading) return <Spinner />;
  if (error) return <ErrorState />;

  return <ProductDetail product={data} onUpdate={mutation.mutate} />;
}

服务器状态不应在全局 UI 存储中重复。 TanStack Query 处理缓存失效、请求重复数据删除、后台重新获取、分页、无限滚动和带回滚的乐观更新。


决策框架

国家类别示例推荐工具
组件本地表单输入、切换、计数器useState / useReducer
全球用户界面模态框、侧边栏、主题、区域设置祖斯坦/佐泰
服务器状态API数据、数据库结果TanStack 查询
URL 状态路由参数、搜索参数路由器原语

性能模式

渲染隔离是最重要的考虑因素。 Zustand(选择器)和 Jotai(原子)等库通过设计最小化了重新渲染范围。永远不要将快速变化的状态放在上下文中接近稳定状态的位置 - 将它们分成单独的提供者。

// Efficient: narrow selector, stable reference
const isDark = useThemeStore((s) => s.isDark);

// Inefficient: full object, unstable reference
const { isDark, toggleTheme } = useThemeStore();

React 18 的自动批处理意味着同一事件处理程序中的多个状态更新会产生一次重新渲染。策略性地使用 useMemouseCallback (而不是作为一揽子规则),以防止在将对象和函数作为 props 传递时不必要的子级重新渲染。


迁移策略

从 Redux 这样的整体存储迁移时,请使用并行方法:

  1. 首先迁移服务器状态(Redux → TanStack 查询 API 数据)。这通常会从 Redux 存储中删除最大的切片。
  2. 使用小型 Zustand 存储以及现有 Redux 逐步替换全局 UI 状态。
  3. 在过渡期间使用适配器层,以便两个存储可以共存。
  4. 在完全切换之前独立验证每个模块。
  5. 使用 React DevTools Profiler 测量每个阶段的包大小和渲染性能。

结论

到 2024 年,还没有最好的状态管理工具。正确的选择取决于国家的类别。使用 useState 来处理组件本地问题,使用 Zustand 或 Jotai 来处理全局 UI 状态,使用 TanStack 查询来处理服务器数据,使用路由器原语来处理 URL 状态。结合每个类别的正确工具可以生成高性能、可维护且易于推理的应用程序。