react-frontend-expert
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact 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 pointsrc/
├── 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 pointComponent 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 and composition over deep prop drilling
children - Never use — use plain function syntax
React.FC
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
- 优先使用和组合模式,避免深层Props透传
children - 禁止使用——使用普通函数语法
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使用规则
- Only call hooks at the top level — never inside loops, conditions, or nested functions
- Only call hooks from React function components or custom hooks
- Custom hooks must start with
use
- 仅在顶层调用Hooks——切勿在循环、条件语句或嵌套函数中调用
- 仅在React函数组件或自定义Hook中调用Hooks
- 自定义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 calls)
useState - 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 > 0 (default 0 is too aggressive):
staleTime(5 min)staleTime: 5 * 60 * 1000 - Use after mutations — never manual
invalidateQueries()refetch() - Handle all states: ,
isPending,isErrordata - Use factory — prevents key typos and duplication
queryOptions() - Use to prevent queries from running with incomplete parameters
enabled
tsx
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UserCreate) =>
apiClient.post<UserResponse>("/users", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
}TanStack Query规则:
- 设置大于0(默认0过于激进):
staleTime(5分钟)staleTime: 5 * 60 * 1000 - 执行Mutation后使用——切勿手动调用
invalidateQueries()refetch() - 处理所有状态:、
isPending、isErrordata - 使用工厂模式——避免键拼写错误和重复定义
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 in
strict: true— no exceptionstsconfig.json - Never use — use
anyfor truly unknown typesunknown - Use for literal object types
as const - Prefer for extensible types,
interfacefor everything elsetype - Use generics for reusable utility types and hooks
- Export types from directory for shared use
types/
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 - 禁止使用——对于真正未知的类型使用
anyunknown - 对字面量对象类型使用
as const - 优先使用定义可扩展类型,其他场景使用
interfacetype - 为可复用的工具类型和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:
- Semantic HTML first: Use ,
<button>,<nav>,<main>— not<article><div onClick> - Labels: Every form input has a with matching
<label>/htmlForid - ARIA only when needed: for icon-only buttons,
aria-labelfor dynamic updates,aria-livefor errorsrole="alert" - Keyboard navigation: All interactive elements reachable via Tab, activatable via Enter/Space
- Focus management: Set focus to main content on route change, trap focus in modals
- Color contrast: Minimum 4.5:1 for normal text, 3:1 for large text
- Alt text: All tags have descriptive
<img>(oraltfor decorative images)alt=""
每个组件必须符合WCAG 2.1 AA标准:
- 优先使用语义化HTML:使用、
<button>、<nav>、<main>——而非<article><div onClick> - 标签设置:每个表单输入框都要有匹配/
htmlFor的id<label> - 仅在必要时使用ARIA:图标按钮使用,动态更新内容使用
aria-label,错误提示使用aria-liverole="alert" - 键盘导航:所有交互元素可通过Tab键聚焦,通过Enter/Space键激活
- 焦点管理:路由切换时将焦点设置到主要内容,模态框中捕获焦点
- 颜色对比度:普通文本最低对比度4.5:1,大文本最低3:1
- 替代文本:所有标签都要有描述性
<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, usefor mutable values that change frequently, or include dependencies in useCallback/useEffect arrays.
useRef -
TanStack Query key collisions: Structure keys hierarchically:for list,
["users"]for detail,["users", id]for filtered list. Use["users", { q, page }]factory to centralize key definitions.queryOptions() -
Infinite re-renders: Common causes: missing dependency arrays, creating new objects/arrays in render (wrap in), state updates in useEffect without proper conditions.
useMemo -
Hydration mismatches: Avoid rendering content that depends on browser-only APIs (window, localStorage) during initial render. Useor check
useEffect.typeof window !== "undefined" -
Memory leaks: Cancel async operations in useEffect cleanup. TanStack Query handles this automatically for queries.
See for annotated component templates.
See for CRUD query patterns.
references/component-templates.mdreferences/tanstack-query-patterns.md-
Hook中的过期闭包:当使用引用状态的回调时,对频繁变化的可变值使用,或在useCallback/useEffect依赖数组中包含相关依赖。
useRef -
TanStack Query键冲突:按层级结构组织键:表示列表,
["users"]表示详情,["users", id]表示过滤后的列表。使用["users", { q, page }]工厂模式集中管理键定义。queryOptions() -
无限重渲染:常见原因:缺少依赖数组、在渲染时创建新对象/数组(用包裹)、useEffect中无适当条件的状态更新。
useMemo -
Hydration不匹配:初始渲染时避免渲染依赖浏览器专属API(window、localStorage)的内容。使用或检查
useEffect。typeof window !== "undefined" -
内存泄漏:在useEffect清理函数中取消异步操作。TanStack Query会自动处理查询的异步操作。
请查看获取带注释的组件模板。
请查看获取CRUD查询模式。
references/component-templates.mdreferences/tanstack-query-patterns.md