Tips & Tricks (更新: 2026/6/2)

用Claude Code整理React状态管理:从Context到TanStack Query

用Claude Code整理React状态管理,区分客户端状态与服务端状态,并比较Context、Zustand、Jotai和TanStack Query。

用Claude Code整理React状态管理:从Context到TanStack Query

React 状态管理最容易出问题的地方,不是没有用库,而是把所有会变化的数据都当成同一种东西。输入框内容、弹窗开关、购物车、主题设置、从 API 取回的商品列表,都叫 state,但它们的来源、生命周期和失败方式完全不同。

Claude Code 可以帮助重构这些代码,但前提是你给它清晰边界。如果只说“帮我优化状态管理”,它可能会把所有东西塞进一个全局 store,或者把 API 数据复制到客户端缓存里。更安全的做法是先让 Claude Code 盘点状态,再决定哪些保留在 React,哪些交给 Zustand、Jotai 或 TanStack Query。

这篇文章用初学者能理解的方式说明判断标准,并给出 todo、cart、settings、商品列表四个实例,包含可复制的 React/TypeScript 代码、测试、常见坑、官方链接和商业化 CTA。

先区分客户端状态和服务端状态

选择库之前,先回答一个问题:这个状态真正属于谁?属于当前组件,属于浏览器里的用户操作,还是属于服务器?

flowchart TD
  A["React state management"] --> B["Local UI state"]
  A --> C["Shared client state"]
  A --> D["Persisted preferences"]
  A --> E["Server state"]
  B --> B1["input, modal, tab"]
  C --> C1["cart, wizard, editor"]
  D --> D1["theme, density, locale"]
  E --> E1["products, orders, profile from API"]

本地 UI 状态适合放在组件附近,比如 tab、modal、输入中的文字。多个动作组成一个页面流程时,可以用 useReducer。如果远处的子组件都要读写同一个值,可以用 Context 或轻量 store。

客户端共享状态是在浏览器里产生的状态,例如购物车草稿、编辑器草稿、向导步骤。持久化偏好是主题、显示密度、语言等,通常可以存在 localStorage。服务端状态则来自 API,可能被其他用户、后台任务或另一个设备改变,所以需要重新获取、缓存、失效和错误处理。

React 内建能力什么时候够用

React 官方的 Managing State 强调先整理 state 结构、必要时提升到共同父组件,再用 reducer 和 context 扩展。这个顺序非常适合给 Claude Code 当约束。

需求先用什么什么时候考虑库
输入框、弹窗、tabuseState通常不需要
一个页面有多个动作useReducerreducer 被多个页面复用
深层组件需要同一值Context高频更新导致大范围重渲染
购物车、编辑草稿Zustand多个远距离组件都要更新
主题、密度、侧栏Jotai很多小偏好需要组合
商品、订单、资料 APITanStack Query需要缓存、重试、失效、刷新

实务判断可以更简单:同一个值是否在三个以上地方更新?是否跨页面?是否刷新后还要保留?是否来自 API?如果答案都是否,React 内建状态通常就足够。

实例1:todo 用 useReducer 和 Context 开始

todo 列表适合用来练习 reducer,因为它有添加、切换、删除等动作,但还不一定需要服务端同步。下面的写法把状态和 dispatch 分开,方便组件读取,也方便测试。

import {
  createContext,
  useContext,
  useMemo,
  useReducer,
  type Dispatch,
  type ReactNode,
} from "react";

export type Todo = {
  id: string;
  title: string;
  done: boolean;
};

type TodoAction =
  | { type: "added"; title: string }
  | { type: "toggled"; id: string }
  | { type: "deleted"; id: string };

export function todoReducer(todos: Todo[], action: TodoAction): Todo[] {
  switch (action.type) {
    case "added":
      return [
        ...todos,
        { id: crypto.randomUUID(), title: action.title.trim(), done: false },
      ];
    case "toggled":
      return todos.map((todo) =>
        todo.id === action.id ? { ...todo, done: !todo.done } : todo,
      );
    case "deleted":
      return todos.filter((todo) => todo.id !== action.id);
    default:
      return todos;
  }
}

const TodoStateContext = createContext<Todo[] | null>(null);
const TodoDispatchContext = createContext<Dispatch<TodoAction> | null>(null);

export function TodoProvider({ children }: { children: ReactNode }) {
  const [todos, dispatch] = useReducer(todoReducer, []);
  const stateValue = useMemo(() => todos, [todos]);

  return (
    <TodoStateContext.Provider value={stateValue}>
      <TodoDispatchContext.Provider value={dispatch}>
        {children}
      </TodoDispatchContext.Provider>
    </TodoStateContext.Provider>
  );
}

export function useTodos() {
  const value = useContext(TodoStateContext);
  if (value === null) throw new Error("useTodos must be used in TodoProvider");
  return value;
}

export function useTodoDispatch() {
  const value = useContext(TodoDispatchContext);
  if (value === null) {
    throw new Error("useTodoDispatch must be used in TodoProvider");
  }
  return value;
}

这个结构很适合让 Claude Code 小步修改。例如“增加 edited action”“禁止空标题”“给 reducer 写测试”。因为动作名称清晰,review diff 时也更容易看出业务逻辑是否被改坏。

实例2:购物车用 Zustand 跨页面共享

购物车会被商品详情页、header 徽标、购物车抽屉和 checkout 同时使用。只用 Context 也可以实现,但当持久化、选择器和更新函数变多时,Zustand 会更直接。它通过 create 建立 hook store,persist middleware 可以保留刷新后的状态。

import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";

export type CartItem = {
  productId: string;
  name: string;
  price: number;
  quantity: number;
};

type CartState = {
  items: CartItem[];
  currency: "JPY" | "USD";
  addItem: (item: Omit<CartItem, "quantity">) => void;
  removeItem: (productId: string) => void;
  setQuantity: (productId: string, quantity: number) => void;
  clear: () => void;
  total: () => number;
};

export const useCartStore = create<CartState>()(
  persist(
    (set, get) => ({
      items: [],
      currency: "USD",
      addItem: (item) =>
        set((state) => {
          const current = state.items.find(
            (entry) => entry.productId === item.productId,
          );
          if (current) {
            return {
              items: state.items.map((entry) =>
                entry.productId === item.productId
                  ? { ...entry, quantity: entry.quantity + 1 }
                  : entry,
              ),
            };
          }
          return { items: [...state.items, { ...item, quantity: 1 }] };
        }),
      removeItem: (productId) =>
        set((state) => ({
          items: state.items.filter((item) => item.productId !== productId),
        })),
      setQuantity: (productId, quantity) =>
        set((state) => ({
          items: state.items.map((item) =>
            item.productId === productId
              ? { ...item, quantity: Math.max(1, quantity) }
              : item,
          ),
        })),
      clear: () => set({ items: [] }),
      total: () =>
        get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
    }),
    {
      name: "cart-v1",
      storage: createJSONStorage(() => localStorage),
      partialize: (state) => ({
        items: state.items,
        currency: state.currency,
      }),
    },
  ),
);

注意,浏览器里的 cart 不能作为最终价格依据。价格、库存、折扣、税费和支付结果必须由服务器确认。让 Claude Code 改这段时,要明确“只持久化 items 和 currency,不持久化 token 或用户敏感信息”。

实例3:设置界面用 Jotai 拆成小 atom

主题、显示密度、侧边栏开关、预览模式等设置很小,但组合很多。Jotai 的 atom 模型适合这种场景:每个设置一个 atom,展示文本或 CSS class 用派生 atom 计算。

import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";

export const themeAtom = atomWithStorage<"light" | "dark">(
  "settings.theme",
  "light",
);

export const densityAtom = atomWithStorage<"comfortable" | "compact">(
  "settings.density",
  "comfortable",
);

export const sidebarOpenAtom = atom(true);

export const settingsLabelAtom = atom((get) => {
  const theme = get(themeAtom) === "dark" ? "dark theme" : "light theme";
  const density =
    get(densityAtom) === "compact" ? "compact layout" : "comfortable layout";
  return `${theme}, ${density}`;
});

export const resetSettingsAtom = atom(null, (_get, set) => {
  set(themeAtom, "light");
  set(densityAtom, "comfortable");
  set(sidebarOpenAtom, true);
});

Jotai 的坑是拆得过细。设置类状态很适合 atom,但复杂业务流程如果被拆成几十个 atom,反而不容易追踪。API 数据也不要用 atom 当缓存,应该放到 TanStack Query。

实例4:商品和订单交给 TanStack Query

商品列表、订单历史、用户资料都属于服务端状态。TanStack Query v5 的定位就是处理获取、缓存、同步和更新服务端状态。它比手写 useEffect + useState 更适合处理 loading、error、refetch、mutation 和 invalidation。

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

type Product = {
  id: string;
  name: string;
  price: number;
  stock: number;
};

const queryClient = new QueryClient();

export function ProductsProvider({ children }: { children: ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}

async function fetchProducts(category: string): Promise<Product[]> {
  const response = await fetch(`/api/products?category=${category}`);
  if (!response.ok) throw new Error("Failed to fetch products");
  return response.json() as Promise<Product[]>;
}

async function addCartLine(productId: string) {
  const response = await fetch("/api/cart/lines", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ productId }),
  });
  if (!response.ok) throw new Error("Failed to add cart line");
  return response.json();
}

export function useProducts(category: string) {
  return useQuery({
    queryKey: ["products", category],
    queryFn: () => fetchProducts(category),
    staleTime: 60_000,
  });
}

export function useAddCartLine() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: addCartLine,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["cart"] });
    },
  });
}

边界是:Zustand 保存用户在浏览器里的临时购物意图,TanStack Query 获取服务器确认后的商品、购物车、订单和结账状态。不要让 Claude Code 把两者混在一个 store 里。

安全地让 Claude Code 重构

Claude Code 的官方说明中,How Claude Code works 描述了收集上下文、执行、验证的循环。状态管理重构也要按这个节奏来:先调查,后修改,最后验证。

请检查这个 React 应用的状态管理,先不要编辑文件。

目标:
- 区分 client state 和 server state
- 找出可以保留在 useState/useReducer/Context 的部分
- 找出适合 Zustand、Jotai、TanStack Query 的部分

输出:
- 状态清单表
- 重复或派生 state
- 把 API 数据当 client state 保存的地方
- 小步迁移顺序和测试建议

第二步只改一个区域:

只实现 todo 页面迁移到 useReducer + Context。

限制:
- 不改变路由和显示文案
- 不增加依赖
- 添加或更新 reducer 测试
- 最后报告修改文件和验证命令

对于购物车和 API 数据,还要加上领域约束,例如“不要持久化 token”“价格由服务器确认”“服务端状态使用 TanStack Query”。这些约束比“用哪个库”更重要。

测试、坑和下一步

重构状态管理时,先测试纯逻辑。下面的 reducer 测试不需要启动浏览器。

import { describe, expect, it, vi } from "vitest";
import { todoReducer, type Todo } from "./TodoProvider";

describe("todoReducer", () => {
  it("adds a todo with a generated id", () => {
    vi.spyOn(crypto, "randomUUID").mockReturnValue("todo-1");

    const result = todoReducer([], { type: "added", title: "Write tests" });

    expect(result).toEqual([
      { id: "todo-1", title: "Write tests", done: false },
    ]);
  });

  it("toggles a todo", () => {
    const initial: Todo[] = [{ id: "todo-1", title: "Ship", done: false }];

    const result = todoReducer(initial, { type: "toggled", id: "todo-1" });

    expect(result[0].done).toBe(true);
  });
});

常见坑有五个:把所有东西都放进全局 store;把 API 数据放进 Zustand 后手写缓存;同时保存 itemstotalPrice 这类派生值;把敏感信息放进 localStorage;没有在刷新、失败请求、低速网络、另一个 tab 中验证。

官方资料可以看 React Managing StateZustand persistJotaiTanStack Query overviewClaude Code best practices。站内继续阅读可以看更好的提示词指南Claude Code 生产力技巧

如果你想把这套流程变成团队标准,可以从 ClaudeCodeLab 产品与模板整理 CLAUDE.md、review prompt 和测试命令;真实项目迁移可以从 training / consultation 开始。

实际试用后,最有效的顺序是先处理 server state。API 数据移到 TanStack Query 后,Zustand 只负责真正的客户端状态,Jotai 只负责设置偏好。先让 Claude Code 输出状态清单和迁移顺序,再让它改代码,review 的负担明显小很多。

#Claude Code #React #状态管理 #Zustand #前端
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。