Use Cases (更新: 2026/6/2)

用 Claude Code 做生产级 React 开发:组件边界、Hooks、测试与审查

用 Claude Code 更安全地开发 React:组件、Hooks、表单、数据获取、测试与无障碍全流程。

用 Claude Code 做生产级 React 开发:组件边界、Hooks、测试与审查

Claude Code 能让 React 页面生成得很快,但生产团队真正需要的不是“更多组件”,而是边界清晰、可测试、可审查的改动。 如果只说“做一个漂亮的后台页面”,AI 很容易生成一堆看似完整、实际难维护的组件。 更稳的做法是先给 Claude Code 组件责任、状态位置、数据类型和验证命令。

本文用 React + TypeScript 说明一套可落地流程:组件边界、props、state、自定义 hook、表单、数据获取、Testing Library、无障碍、性能,以及审查 prompt。官方资料建议参考 React 文档React Testing LibraryMDN ARIAClaude Code 文档。站内延伸可以看 TypeScript 技巧测试策略无障碍实现性能优化

先定义 Claude Code 的任务边界

props 可以理解为“父组件传给子组件的输入”,state 是“界面需要记住的值”,自定义 hook 是“把状态和副作用逻辑抽出来复用的函数”。副作用包括 API 请求、定时器、localStorage 等不只是渲染 UI 的行为。把这些术语说清楚,Claude Code 的输出会更接近团队规范。

需要给的信息示例作用
页面责任用户列表、角色筛选、启停账号避免一个组件做完所有事
状态位置父组件、URL、custom hook、server cache避免 useState 到处散落
数据契约User 类型、API 响应、空状态props 和测试更具体
验证方式npm testnpm run build、键盘检查审查时有证据

真实使用场景

第一个场景是内部管理后台,例如用户、订单、发票、客服工单。Claude Code 适合把页面拆成过滤器、表格、行操作、空状态和错误状态,但要明确“不要改 API 类型”“不要新增通用 Table 框架”。

第二个场景是表单,例如个人资料编辑、咨询预约、课程报名、结账信息。要让 Claude Code 保留 label、错误提示、提交中状态和失败重试逻辑。复杂表单可以继续看 React Hook Form 指南

第三个场景是数据获取页面,例如搜索结果、通知、dashboard。server state,也就是“服务器上的状态”,不要和输入框展开状态、弹窗开关这类临时 UI state 混在一起。使用 TanStack Query 时可以参考 TanStack Query 指南,小型客户端状态可以参考 Zustand 状态管理

第四个场景是代码审查。与其让 Claude Code 继续生成新组件,不如让它检查 diff:组件是否太大、Effect 是否必要、表单是否可访问、测试是否只靠快照、列表是否会过度重渲染。

组件结构图

UserAdminPage
  -> UserFilters: 搜索和角色筛选
  -> UserTable: 表格、排序提示、行操作
  -> UserStatusBadge: 只负责状态显示
  -> UserEditDialog: 编辑表单
  -> useUsers: 数据获取、刷新、错误状态

判断边界的简单方法是:能否用一句话解释这个组件的 props。如果解释不清,通常是责任太多;如果只用一次又没有独立行为,可能暂时不需要抽组件。

可复制的类型化 React 组件

下面的例子可以放进 Vite + React + TypeScript 项目中使用。它不负责请求 API,只展示数据并把事件传回父层,因此更容易测试。

import type { FormEvent } from "react";

export type UserRole = "admin" | "editor" | "viewer";

export type User = {
  id: string;
  name: string;
  email: string;
  role: UserRole;
  active: boolean;
};

type UserTableProps = {
  users: User[];
  selectedRole: "all" | UserRole;
  onRoleChange: (role: "all" | UserRole) => void;
  onToggleActive: (id: string) => void;
};

export function UserTable({
  users,
  selectedRole,
  onRoleChange,
  onToggleActive,
}: UserTableProps) {
  const filteredUsers =
    selectedRole === "all"
      ? users
      : users.filter((user) => user.role === selectedRole);

  function handleRoleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    onRoleChange(formData.get("role") as "all" | UserRole);
  }

  return (
    <section aria-labelledby="user-table-title">
      <h2 id="user-table-title">Team members</h2>
      <form onSubmit={handleRoleSubmit} style={{ marginBottom: 12 }}>
        <label htmlFor="role">Filter by role </label>
        <select id="role" name="role" defaultValue={selectedRole}>
          <option value="all">All</option>
          <option value="admin">Admin</option>
          <option value="editor">Editor</option>
          <option value="viewer">Viewer</option>
        </select>
        <button type="submit">Apply</button>
      </form>
      {filteredUsers.length === 0 ? (
        <p role="status">No users match this filter.</p>
      ) : (
        <table>
          <thead>
            <tr>
              <th scope="col">Name</th>
              <th scope="col">Email</th>
              <th scope="col">Role</th>
              <th scope="col">Status</th>
              <th scope="col">Action</th>
            </tr>
          </thead>
          <tbody>
            {filteredUsers.map((user) => (
              <tr key={user.id}>
                <td>{user.name}</td>
                <td>{user.email}</td>
                <td>{user.role}</td>
                <td>{user.active ? "Active" : "Paused"}</td>
                <td>
                  <button type="button" onClick={() => onToggleActive(user.id)}>
                    {user.active ? "Pause" : "Activate"}
                  </button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </section>
  );
}

自定义 hook 与测试

把请求逻辑放进 hook,可以让 UI 组件保持简单。

import { useEffect, useState } from "react";
import type { User } from "./UserTable";

type UsersState =
  | { status: "loading"; users: User[]; error: null }
  | { status: "success"; users: User[]; error: null }
  | { status: "error"; users: User[]; error: string };

export function useUsers(endpoint: string) {
  const [state, setState] = useState<UsersState>({
    status: "loading",
    users: [],
    error: null,
  });

  useEffect(() => {
    const controller = new AbortController();

    async function loadUsers() {
      setState({ status: "loading", users: [], error: null });
      try {
        const response = await fetch(endpoint, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const users = (await response.json()) as User[];
        setState({ status: "success", users, error: null });
      } catch (error) {
        if (error instanceof DOMException && error.name === "AbortError") return;
        setState({ status: "error", users: [], error: error instanceof Error ? error.message : "Unknown error" });
      }
    }

    void loadUsers();
    return () => controller.abort();
  }, [endpoint]);

  return state;
}

Testing Library 测试应从用户视角出发,优先使用 label、role 和可见文字。

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { UserTable, type User } from "./UserTable";

const users: User[] = [
  { id: "1", name: "Masa", email: "masa@example.com", role: "admin", active: true },
  { id: "2", name: "Aiko", email: "aiko@example.com", role: "viewer", active: false },
];

describe("UserTable", () => {
  it("filters users and toggles active status", async () => {
    const user = userEvent.setup();
    const onRoleChange = vi.fn();
    const onToggleActive = vi.fn();

    render(<UserTable users={users} selectedRole="all" onRoleChange={onRoleChange} onToggleActive={onToggleActive} />);

    await user.selectOptions(screen.getByLabelText(/filter by role/i), "viewer");
    await user.click(screen.getByRole("button", { name: /apply/i }));
    await user.click(screen.getByRole("button", { name: /activate/i }));

    expect(onRoleChange).toHaveBeenCalledWith("viewer");
    expect(onToggleActive).toHaveBeenCalledWith("2");
  });
});

审查 Prompt

请审查这个 React + TypeScript diff。
重点检查组件边界、props 是否过多、useEffect 是否必要、UI state 和 server state 是否混在一起、表单 label 和错误提示、Testing Library 覆盖、以及大列表的重渲染风险。
不要主动新增库;如需建议,只作为建议列出。最后给出 npm test 和 npm run build 的验证清单。

常见坑

第一个坑是过度生成组件。UserNameTextUserEmailText 这类只出现一次、没有行为的组件通常没有价值。第二个坑是滥用 useEffect,把可计算的过滤结果也放进 Effect。第三个坑是无障碍缺失,例如只用 placeholder、图标按钮没有名称、错误只靠颜色表达。第四个坑是性能优化做成表演,到处加 memouseMemo,却没有先确认瓶颈。

CTA 与验证结果

团队采用 Claude Code 时,建议把组件边界、测试命令、无障碍规则和“不要过度抽象”的原则写进 CLAUDE.md。如果你想把这套流程应用到真实 React 仓库,可以使用 ClaudeCodeLab 培训与咨询,一起整理 UI 审查、Testing Library、性能验证和咨询/报名/购买 CTA。

实际试用结果:Masa 在内容站后台 UI 改造中发现,先让 Claude Code 明确边界、状态归属和测试期望,再生成代码,后续 props 清理和无障碍返工明显减少。React 中最稳定的用法不是生成更多,而是生成更小、可验证的 diff。

#Claude Code #React #frontend #components #Hooks
免费

免费 PDF: Claude Code 速查表

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

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

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

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

Masa

关于作者

Masa

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