mcpField
R
react-frontend-expert

react-frontend-expert

github.com/hieutrtr开发8205 次安装39 周前更新
代码仓库

关于 react-frontend-expert

面向 React/TypeScript 开发的编码技能,指导组件、Hooks、路由、数据获取和表单实现,并涵盖类型规范与无障碍要求。

主要功能

  1. 01函数组件与组件组织规范
  2. 02自定义 Hook 设计模式
  3. 03TanStack Query 数据获取
  4. 04TypeScript 类型约定
  5. 05表单验证与无障碍要求

适用场景

  1. 01创建或修改 React 组件
  2. 02构建带路由的页面
  3. 03实现查询、变更与缓存失效
  4. 04开发带验证的表单

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/hieutrtr/ai1-skills --skill react-frontend-expert

安装后在 Claude Code 中输入/react-frontend-expert确认已加载。

附带文件references/component-templates.mdreferences/tanstack-query-patterns.md

详细介绍

React 前端专家

适用场景

在以下情况下启用此技能:

  • 创建或修改 React 组件(仅函数组件)
  • 编写自定义 Hook(useXxx)
  • 构建带路由的页面
  • 使用 TanStack Query 实现数据获取
  • 实现带验证的表单
  • 为 React/TypeScript 应用设置项目结构

以下情况请勿使用此技能:

  • 编写组件或 Hook 测试(使用 react-testing-patterns)
  • 端到端浏览器测试(使用 e2e-testing)
  • API 契约设计(使用 api-design-patterns)
  • 后端实现(使用 python-backend-expert)
  • 部署或 CI/CD(使用 deployment-pipeline)

说明

项目结构
src/
├── api/                  # API client functions and query options
│   ├── client.ts         # Axios/fetch instance with interceptors
│   ├── users.ts          # User API functions + query options
│   └── posts.ts
├── components/           # Shared, reusable UI components
│   ├── Button.tsx
│   ├── Modal.tsx
│   ├── Table/
│   │   ├── Table.tsx
│   │   └── TablePagination.tsx
│   └── Form/
│       ├── Input.tsx
│       └── Select.tsx
├── features/             # Domain-specific feature components
│   ├── users/
│   │   ├── UserList.tsx
│   │   └── UserProfile.tsx
│   └── posts/
│       └── PostEditor.tsx
├── hooks/                # Custom hooks
│   ├── useAuth.ts
│   ├── useDebounce.ts
│   └── usePagination.ts
├── layouts/              # Layout components
│   ├── MainLayout.tsx
│   └── AuthLayout.tsx
├── pages/                # Route-level page components
│   ├── HomePage.tsx
│   ├── LoginPage.tsx
│   └── users/
│       ├── UserListPage.tsx
│       └── UserDetailPage.tsx
├── types/                # Shared TypeScript types
│   ├── api.ts            # API response types
│   └── user.ts
├── App.tsx               # Root component with providers and router
└── main.tsx              # Entry point
组件结构
仅使用函数组件
interface UserCardProps {
  user: User;
  onEdit: (userId: number) => void;
  showEmail?: boolean;
}

export function UserCard({ user, onEdit, showEmail = false }: UserCardProps) {
  return (
    <article className="user-card">
      <h3>{user.displayName}</h3>
      {showEmail && <p>{user.email}</p>}
      <button type="button" onClick={() => onEdit(user.id)}>
        Edit
      </button>
    </article>
  );
}

组件规则:

  • 共享组件使用命名导出:export function Button
  • 页面组件使用默认导出:export default function UserListPage
  • Props 接口命名为 {Component}Props
  • 在函数签名中解构 Props
  • 组件保持在 200 行以内——超过时提取子组件或 Hook
  • 使用 children 和组合,而不是深层 Prop 逐级传递
  • 绝不使用 React.FC——使用普通函数语法
组件文件组织

对于复杂组件,将相关文件放在同一目录:

UserProfile/
├── UserProfile.tsx       # Main component
├── UserProfile.css       # Styles (or .module.css)
├── UserAvatar.tsx        # Sub-component
└── index.ts              # Re-export: export { UserProfile } from './UserProfile'
Hook 规则与自定义 Hook
Hook 规则
  1. 仅在顶层调用 Hook——绝不在循环、条件或嵌套函数中调用
  2. 仅从 React 函数组件或自定义 Hook 中调用 Hook
  3. 自定义 Hook 的名称必须以 use 开头
自定义 Hook 模式

useDebounce:

export function useDebounce<T>(value: T, delayMs: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delayMs);
    return () => clearTimeout(timer);
  }, [value, delayMs]);

  return debouncedValue;
}

useAuth:

interface AuthContext {
  user: User | null;
  isAuthenticated: boolean;
  login: (credentials: LoginCredentials) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContext | null>(null);

export function useAuth(): AuthContext {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within AuthProvider");
  }
  return context;
}

usePagination:

interface PaginationState {
  cursor: string | null;
  hasMore: boolean;
  goToNext: (nextCursor: string) => void;
  reset: () => void;
}

export function usePagination(): PaginationState {
  const [cursor, setCursor] = useState<string | null>(null);
  const [hasMore, setHasMore] = useState(true);

  return {
    cursor,
    hasMore,
    goToNext: (nextCursor: string) => {
      setCursor(nextCursor);
    },
    reset: () => {
      setCursor(null);
      setHasMore(true);
    },
  };
}

何时提取自定义 Hook:

  • 逻辑在 2 个或更多组件间重复使用
  • 组件的状态管理较复杂(调用 useState 超过 3 次)
  • 副作用需要封装(订阅、计时器)
  • 数据获取逻辑可以共享
使用 TanStack Query 获取数据
查询选项工厂(推荐)

集中定义查询键和函数,以避免键冲突:

// api/users.ts
import { queryOptions } from "@tanstack/react-query";

export const userQueries = {
  all: () =>
    queryOptions({
      queryKey: ["users"],
      queryFn: () => apiClient.get<UserListResponse>("/users"),
    }),

  detail: (userId: number) =>
    queryOptions({
      queryKey: ["users", userId],
      queryFn: () => apiClient.get<UserResponse>(`/users/${userId}`),
    }),

  search: (query: string) =>
    queryOptions({
      queryKey: ["users", "search", query],
      queryFn: () => apiClient.get<UserListResponse>(`/users?q=${query}`),
      enabled: query.length > 0,
    }),
};
在组件中使用查询
export function UserDetailPage({ userId }: { userId: number }) {
  const { data: user, isPending, isError, error } = useQuery(
    userQueries.detail(userId)
  );

  if (isPending) return <Spinner />;
  if (isError) return <ErrorMessage error={error} />;

  return <UserProfile user={user} />;
}
使用缓存失效的变更操作
export function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: UserCreate) =>
      apiClient.post<UserResponse>("/users", data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });
}

TanStack Query 规则:

  • 将 staleTime 设为大于 0(默认值 0 过于激进):staleTime: 5 * 60 * 1000(5 分钟)
  • 在变更操作后使用 invalidateQueries()——绝不手动调用 refetch()
  • 处理所有状态:isPending、isError、data
  • 使用 queryOptions() 工厂——避免键名拼写错误和重复定义
  • 使用 enabled,避免在参数不完整时运行查询
TypeScript 规范
// Use `interface` for object shapes (components props, API responses)
interface User {
  id: number;
  email: string;
  displayName: string;
  role: "admin" | "editor" | "member";
}

// Use `type` for unions, intersections, and computed types
type UserRole = User["role"];
type CreateOrUpdate = UserCreate | UserUpdate;

// Discriminated unions for state machines
type AsyncState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

TypeScript 规则:

  • 在 tsconfig.json 中启用 strict: true——无一例外
  • 绝不使用 any——对于确实未知的类型,使用 unknown
  • 对字面量对象类型使用 as const
  • 可扩展类型优先使用 interface,其他情况使用 type
  • 对可复用的实用类型和 Hook 使用泛型
  • 在 types/ 目录中导出供共享使用的类型
表单处理
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const userSchema = z.object({
  email: z.string().email("Invalid email"),
  displayName: z.string().min(1, "Required").max(100),
  role: z.enum(["admin", "editor", "member"]),
});

type UserFormData = z.infer<typeof userSchema>;

export function UserForm({ onSubmit }: { onSubmit: (data: UserFormData) => void }) {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<UserFormData>({
    resolver: zodResolver(userSchema),
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <label htmlFor="email">Email</label>
      <input id="email" type="email" {...register("email")} aria-invalid={!!errors.email} />
      {errors.email && <span role="alert">{errors.email.message}</span>}

      <label htmlFor="displayName">Name</label>
      <input id="displayName" {...register("displayName")} aria-invalid={!!errors.displayName} />
      {errors.displayName && <span role="alert">{errors.displayName.message}</span>}

      <button type="submit" disabled={isSubmitting}>Save</button>
    </form>
  );
}
无障碍要求

每个组件都必须符合 WCAG 2.1 AA:

  1. 优先使用语义化 HTML: 使用 <button>、<nav>、<main>、<article>——而不是 <div onClick>
  2. 标签: 每个表单输入框都必须有 <label>,且 htmlFor 与 id 相匹配
  3. 仅在需要时使用 ARIA: 图标按钮使用 aria-label,动态更新使用 aria-live,错误提示使用 role="alert"
  4. 键盘导航: 所有交互元素都可通过 Tab 键访问,并可通过 Enter/Space 激活
  5. 焦点管理: 路由变更时将焦点设到主要内容,在模态框中限制焦点
  6. 颜色对比度: 普通文本至少为 4.5:1,大号文本至少为 3:1
  7. 替代文本: 所有 <img> 标签都必须有描述性 alt(装饰性图片使用 alt="")

示例

带搜索和分页的用户列表页
export default function UserListPage() {
  const [search, setSearch] = useState("");
  const debouncedSearch = useDebounce(search, 300);
  const pagination = usePagination();

  const { data, isPending } = useQuery(
    userQueries.list({ q: debouncedSearch, cursor: pagination.cursor })
  );

  return (
    <main>
      <h1>Users</h1>
      <input
        type="search"
        value={search}
        onChange={(e) => { setSearch(e.target.value); pagination.reset(); }}
        placeholder="Search users..."
        aria-label="Search users"
      />
      {isPending ? <Spinner /> : (
        <>
          <UserTable users={data.items} />
          {data.hasMore && (
            <button onClick={() => pagination.goToNext(data.nextCursor)}>
              Load more
            </button>
          )}
        </>
      )}
    </main>
  );
}

边缘情况

  • Hook 中的闭包过期: 使用引用状态的回调时,对于频繁变化的可变值,请使用 useRef,或将依赖项添加到 useCallback/useEffect 数组中。

  • TanStack Query 键冲突: 按层级组织键:列表使用 ["users"],详情使用 ["users", id],筛选列表使用 ["users", { q, page }]。使用 queryOptions() 工厂集中定义键。

  • 无限重新渲染: 常见原因包括:缺少依赖项数组、在渲染时创建新对象/数组(使用 useMemo 包装)、在 useEffect 中未设置适当条件就更新状态。

  • 水合不匹配: 避免在初始渲染期间渲染依赖浏览器专属 API(window、localStorage)的内容。使用 useEffect,或检查 typeof window !== "undefined"。

  • 内存泄漏: 在 useEffect 清理函数中取消异步操作。TanStack Query 会自动处理查询的取消。

请参阅 references/component-templates.md,了解带注释的组件模板。 请参阅 references/tanstack-query-patterns.md,了解 CRUD 查询模式。

评论

支持 Markdown