react-frontend-expert

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Frontend Expert

React前端专家

When to Use

使用场景

Activate this skill when:
  • Creating or modifying React components (functional components only)
  • Writing custom hooks (
    useXxx
    )
  • Building pages with routing
  • Implementing data fetching with TanStack Query
  • Handling forms with validation
  • Setting up project structure for a React/TypeScript application
Do NOT use this skill for:
  • Writing component or hook tests (use
    react-testing-patterns
    )
  • E2E browser testing (use
    e2e-testing
    )
  • API contract design (use
    api-design-patterns
    )
  • Backend implementation (use
    python-backend-expert
    )
  • Deployment or CI/CD (use
    deployment-pipeline
    )
在以下场景中激活该技能:
  • 创建或修改React组件(仅函数式组件)
  • 编写自定义Hooks(
    useXxx
    格式)
  • 构建带路由的页面
  • 基于TanStack Query实现数据获取逻辑
  • 处理带验证的表单
  • 为React/TypeScript应用设置项目结构
以下场景请勿使用该技能:
  • 编写组件或Hook测试(请使用
    react-testing-patterns
  • 浏览器端到端测试(请使用
    e2e-testing
  • API契约设计(请使用
    api-design-patterns
  • 后端实现(请使用
    python-backend-expert
  • 部署或CI/CD流程(请使用
    deployment-pipeline

Instructions

实施指南

Project Structure

项目结构

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
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

Component Structure

组件结构

Functional Components Only

仅使用函数式组件

tsx
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>
  );
}
Component rules:
  • Named exports for shared components:
    export function Button
  • Default exports for page components:
    export default function UserListPage
  • Props interface named
    {Component}Props
  • Destructure props in function signature
  • Keep components under 200 lines — extract sub-components or hooks when larger
  • Use
    children
    and composition over deep prop drilling
  • Never use
    React.FC
    — use plain function syntax
tsx
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
    和组合模式,避免深层Props透传
  • 禁止使用
    React.FC
    ——使用普通函数语法

Component File Organization

组件文件组织

For complex components, co-locate related files:
UserProfile/
├── UserProfile.tsx       # Main component
├── UserProfile.css       # Styles (or .module.css)
├── UserAvatar.tsx        # Sub-component
└── index.ts              # Re-export: export { UserProfile } from './UserProfile'
对于复杂组件,将相关文件放在同一目录下:
UserProfile/
├── UserProfile.tsx       # Main component
├── UserProfile.css       # Styles (or .module.css)
├── UserAvatar.tsx        # Sub-component
└── index.ts              # Re-export: export { UserProfile } from './UserProfile'

Hooks Rules and Custom Hooks

Hooks规则与自定义Hook

Rules of Hooks

Hooks使用规则

  1. Only call hooks at the top level — never inside loops, conditions, or nested functions
  2. Only call hooks from React function components or custom hooks
  3. Custom hooks must start with
    use
  1. 仅在顶层调用Hooks——切勿在循环、条件语句或嵌套函数中调用
  2. 仅在React函数组件或自定义Hook中调用Hooks
  3. 自定义Hook必须以
    use
    开头命名

Custom Hook Patterns

自定义Hook模式

useDebounce:
tsx
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:
tsx
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:
tsx
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);
    },
  };
}
When to extract a custom hook:
  • Logic is reused across 2+ components
  • Component has complex state management (>3
    useState
    calls)
  • Side effects need encapsulation (subscriptions, timers)
  • Data fetching logic can be shared
useDebounce:
tsx
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:
tsx
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:
tsx
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个及以上组件中复用
  • 组件包含复杂状态管理(超过3个
    useState
    调用)
  • 需要封装副作用(订阅、定时器等)
  • 数据获取逻辑可被共享

Data Fetching with TanStack Query

基于TanStack Query的数据获取

Query Options Factory (Recommended)

查询选项工厂(推荐)

Centralize query key and function definitions to prevent key collisions:
tsx
// 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,
    }),
};
集中管理查询键和函数定义,避免键冲突:
tsx
// 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,
    }),
};

Using Queries in Components

在组件中使用查询

tsx
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} />;
}
tsx
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} />;
}

Mutations with Cache Invalidation

带缓存失效的Mutation

tsx
export function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: UserCreate) =>
      apiClient.post<UserResponse>("/users", data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });
}
TanStack Query rules:
  • Set
    staleTime
    > 0 (default 0 is too aggressive):
    staleTime: 5 * 60 * 1000
    (5 min)
  • Use
    invalidateQueries()
    after mutations — never manual
    refetch()
  • Handle all states:
    isPending
    ,
    isError
    ,
    data
  • Use
    queryOptions()
    factory — prevents key typos and duplication
  • Use
    enabled
    to prevent queries from running with incomplete parameters
tsx
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分钟)
  • 执行Mutation后使用
    invalidateQueries()
    ——切勿手动调用
    refetch()
  • 处理所有状态:
    isPending
    isError
    data
  • 使用
    queryOptions()
    工厂模式——避免键拼写错误和重复定义
  • 使用
    enabled
    参数防止参数不完整时执行查询

TypeScript Conventions

TypeScript规范

tsx
// 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 rules:
  • Enable
    strict: true
    in
    tsconfig.json
    — no exceptions
  • Never use
    any
    — use
    unknown
    for truly unknown types
  • Use
    as const
    for literal object types
  • Prefer
    interface
    for extensible types,
    type
    for everything else
  • Use generics for reusable utility types and hooks
  • Export types from
    types/
    directory for shared use
tsx
// 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/
    目录导出

Form Handling

表单处理

tsx
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>
  );
}
tsx
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>
  );
}

Accessibility Requirements

可访问性要求

Every component must meet WCAG 2.1 AA:
  1. Semantic HTML first: Use
    <button>
    ,
    <nav>
    ,
    <main>
    ,
    <article>
    — not
    <div onClick>
  2. Labels: Every form input has a
    <label>
    with matching
    htmlFor
    /
    id
  3. ARIA only when needed:
    aria-label
    for icon-only buttons,
    aria-live
    for dynamic updates,
    role="alert"
    for errors
  4. Keyboard navigation: All interactive elements reachable via Tab, activatable via Enter/Space
  5. Focus management: Set focus to main content on route change, trap focus in modals
  6. Color contrast: Minimum 4.5:1 for normal text, 3:1 for large text
  7. Alt text: All
    <img>
    tags have descriptive
    alt
    (or
    alt=""
    for decorative images)
每个组件必须符合WCAG 2.1 AA标准:
  1. 优先使用语义化HTML:使用
    <button>
    <nav>
    <main>
    <article>
    ——而非
    <div onClick>
  2. 标签设置:每个表单输入框都要有匹配
    htmlFor
    /
    id
    <label>
  3. 仅在必要时使用ARIA:图标按钮使用
    aria-label
    ,动态更新内容使用
    aria-live
    ,错误提示使用
    role="alert"
  4. 键盘导航:所有交互元素可通过Tab键聚焦,通过Enter/Space键激活
  5. 焦点管理:路由切换时将焦点设置到主要内容,模态框中捕获焦点
  6. 颜色对比度:普通文本最低对比度4.5:1,大文本最低3:1
  7. 替代文本:所有
    <img>
    标签都要有描述性
    alt
    属性(装饰性图片使用
    alt=""

Examples

示例

User List Page with Search and Pagination

带搜索和分页的用户列表页面

tsx
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>
  );
}
tsx
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>
  );
}

Edge Cases

边缘情况

  • Stale closures in hooks: When using callbacks that reference state, use
    useRef
    for mutable values that change frequently, or include dependencies in useCallback/useEffect arrays.
  • TanStack Query key collisions: Structure keys hierarchically:
    ["users"]
    for list,
    ["users", id]
    for detail,
    ["users", { q, page }]
    for filtered list. Use
    queryOptions()
    factory to centralize key definitions.
  • Infinite re-renders: Common causes: missing dependency arrays, creating new objects/arrays in render (wrap in
    useMemo
    ), state updates in useEffect without proper conditions.
  • Hydration mismatches: Avoid rendering content that depends on browser-only APIs (window, localStorage) during initial render. Use
    useEffect
    or check
    typeof window !== "undefined"
    .
  • Memory leaks: Cancel async operations in useEffect cleanup. TanStack Query handles this automatically for queries.
See
references/component-templates.md
for annotated component templates. See
references/tanstack-query-patterns.md
for CRUD query patterns.
  • Hook中的过期闭包:当使用引用状态的回调时,对频繁变化的可变值使用
    useRef
    ,或在useCallback/useEffect依赖数组中包含相关依赖。
  • TanStack Query键冲突:按层级结构组织键:
    ["users"]
    表示列表,
    ["users", id]
    表示详情,
    ["users", { q, page }]
    表示过滤后的列表。使用
    queryOptions()
    工厂模式集中管理键定义。
  • 无限重渲染:常见原因:缺少依赖数组、在渲染时创建新对象/数组(用
    useMemo
    包裹)、useEffect中无适当条件的状态更新。
  • Hydration不匹配:初始渲染时避免渲染依赖浏览器专属API(window、localStorage)的内容。使用
    useEffect
    或检查
    typeof window !== "undefined"
  • 内存泄漏:在useEffect清理函数中取消异步操作。TanStack Query会自动处理查询的异步操作。
请查看
references/component-templates.md
获取带注释的组件模板。 请查看
references/tanstack-query-patterns.md
获取CRUD查询模式。