关于 react-frontend-expert
面向 React/TypeScript 开发的编码技能,指导组件、Hooks、路由、数据获取和表单实现,并涵盖类型规范与无障碍要求。
主要功能
- 01函数组件与组件组织规范
- 02自定义 Hook 设计模式
- 03TanStack Query 数据获取
- 04TypeScript 类型约定
- 05表单验证与无障碍要求
适用场景
- 01创建或修改 React 组件
- 02构建带路由的页面
- 03实现查询、变更与缓存失效
- 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 规则
- 仅在顶层调用 Hook——绝不在循环、条件或嵌套函数中调用
- 仅从 React 函数组件或自定义 Hook 中调用 Hook
- 自定义 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:
- 优先使用语义化 HTML: 使用
<button>、<nav>、<main>、<article>——而不是<div onClick> - 标签: 每个表单输入框都必须有
<label>,且htmlFor与id相匹配 - 仅在需要时使用 ARIA: 图标按钮使用
aria-label,动态更新使用aria-live,错误提示使用role="alert" - 键盘导航: 所有交互元素都可通过 Tab 键访问,并可通过 Enter/Space 激活
- 焦点管理: 路由变更时将焦点设到主要内容,在模态框中限制焦点
- 颜色对比度: 普通文本至少为 4.5:1,大号文本至少为 3:1
- 替代文本: 所有
<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 查询模式。