react-frontend-expert by hieutrtr/ai1-skills
npx skills add https://github.com/hieutrtr/ai1-skills --skill react-frontend-expert在以下情况时激活此技能:
useXxx)不要在以下情况使用此技能:
react-testing-patterns)e2e-testing)api-design-patterns)python-backend-expert)deployment-pipeline)广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
src/
├── api/ # API 客户端函数和查询选项
│ ├── client.ts # 带有拦截器的 Axios/fetch 实例
│ ├── users.ts # 用户 API 函数 + 查询选项
│ └── posts.ts
├── components/ # 共享、可复用的 UI 组件
│ ├── Button.tsx
│ ├── Modal.tsx
│ ├── Table/
│ │ ├── Table.tsx
│ │ └── TablePagination.tsx
│ └── Form/
│ ├── Input.tsx
│ └── Select.tsx
├── features/ # 特定领域的功能组件
│ ├── users/
│ │ ├── UserList.tsx
│ │ └── UserProfile.tsx
│ └── posts/
│ └── PostEditor.tsx
├── hooks/ # 自定义钩子
│ ├── useAuth.ts
│ ├── useDebounce.ts
│ └── usePagination.ts
├── layouts/ # 布局组件
│ ├── MainLayout.tsx
│ └── AuthLayout.tsx
├── pages/ # 路由级别的页面组件
│ ├── HomePage.tsx
│ ├── LoginPage.tsx
│ └── users/
│ ├── UserListPage.tsx
│ └── UserDetailPage.tsx
├── types/ # 共享的 TypeScript 类型
│ ├── api.ts # API 响应类型
│ └── user.ts
├── App.tsx # 包含提供者和路由器的根组件
└── main.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 Buttonexport default function UserListPage{Component}Propschildren 和组合,而非深层属性透传React.FC — 使用普通的函数语法对于复杂组件,将相关文件放在一起:
UserProfile/
├── UserProfile.tsx # 主组件
├── UserProfile.css # 样式(或 .module.css)
├── UserAvatar.tsx # 子组件
└── index.ts # 重新导出:export { UserProfile } from './UserProfile'
use 开头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);
},
};
}
何时提取自定义钩子:
useState 调用)集中定义查询键和函数,防止键冲突:
// 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、dataqueryOptions() 工厂 — 防止键拼写错误和重复enabled 防止查询在不完整的参数下运行// 使用 `interface` 定义对象形状(组件属性、API 响应)
interface User {
id: number;
email: string;
displayName: string;
role: "admin" | "editor" | "member";
}
// 使用 `type` 定义联合类型、交叉类型和计算类型
type UserRole = User["role"];
type CreateOrUpdate = UserCreate | UserUpdate;
// 使用可辨识联合类型表示状态机
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
TypeScript 规则:
tsconfig.json 中启用 strict: true — 无例外any — 对于真正未知的类型使用 unknownas constinterface,其他情况使用 typetypes/ 目录导出类型以供共享使用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 标准:
<button>、<nav>、<main>、<article> — 而不是 <div onClick>htmlFor/id 的 <label>aria-label,为动态更新使用 aria-live,为错误信息使用 role="alert"<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>
);
}
钩子中的过时闭包: 当使用引用状态的回调时,对于频繁变化的值使用 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。有关 CRUD 查询模式,请参阅 references/tanstack-query-patterns.md。
每周安装数
78
仓库
GitHub 星标数
8
首次出现
2026年2月4日
安全审计
安装于
codex71
opencode70
github-copilot69
gemini-cli66
kimi-cli62
amp62
Activate this skill when:
useXxx)Do NOT use this skill for:
react-testing-patterns)e2e-testing)api-design-patterns)python-backend-expert)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>
);
}
Component rules:
export function Buttonexport default function UserListPage{Component}Propschildren and composition over deep prop drillingReact.FC — use plain function syntaxFor 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'
useuseDebounce:
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);
},
};
}
When to extract a custom hook:
useState calls)Centralize query key and function definitions to prevent key collisions:
// 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 rules:
staleTime > 0 (default 0 is too aggressive): staleTime: 5 * 60 * 1000 (5 min)invalidateQueries() after mutations — never manual refetch()isPending, isError, dataqueryOptions() factory — prevents key typos and duplicationenabled to prevent queries from running with incomplete parameters// 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:
strict: true in tsconfig.json — no exceptionsany — use unknown for truly unknown typesas const for literal object typesinterface for extensible types, type for everything elsetypes/ directory for shared useimport { 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>
);
}
Every component must meet WCAG 2.1 AA:
<button>, <nav>, <main>, <article> — not <div onClick><label> with matching htmlFor/idaria-label for icon-only buttons, aria-live for dynamic updates, for errorsexport 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>
);
}
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 .
See references/component-templates.md for annotated component templates. See references/tanstack-query-patterns.md for CRUD query patterns.
Weekly Installs
78
Repository
GitHub Stars
8
First Seen
Feb 4, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex71
opencode70
github-copilot69
gemini-cli66
kimi-cli62
amp62
TanStack Query v5 完全指南:React 数据管理、乐观更新、离线支持
2,500 周安装
role="alert"<img> tags have descriptive alt (or alt="" for decorative images)typeof window !== "undefined"Memory leaks: Cancel async operations in useEffect cleanup. TanStack Query handles this automatically for queries.