一键导入
react-idioms
React hooks, Suspense, Server Components, React 19 patterns. For TypeScript see typescript-idioms.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
React hooks, Suspense, Server Components, React 19 patterns. For TypeScript see typescript-idioms.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Structured fault tolerance for coordinator agents. 5-level escalation ladder (Retry → Replace → Skip → Redistribute → Degrade), dead-man timers, degraded completion protocol, and cross-level escalation format. Load when orchestrating agents that may fail.
Structured code review protocol for inspecting code quality against the full rule set. Use when auditing code written by yourself or another agent, during the /audit workflow, or when the user asks for a code review.
Reusable convergence protocol for coordinator agents. Defines the BRIEFING → ITERATE → GATE → CONVERGE loop, context hygiene rules, self-succession protocol, turn budget, and handoff compression. Load when orchestrating multi-iteration workflows.
Pre-flight checklist and post-implementation self-review protocol. Use before generating any code (pre-flight) and after writing code but before verification (self-review) to catch issues early.
MECE task decomposition, file ownership enforcement, DAG-based execution, and safe merge protocol for intra-domain parallel dispatch. The safety invariants that prevent merge chaos when multiple agents write in parallel. Applies recursively at every nesting depth.
Shared protocols for all agents in the multi-agent pipeline: recursive nesting, pre-implementation restatement, parallel dispatch format, and agent definition cascade. Load this skill instead of inlining these protocols in every agent file.
| name | react-idioms |
| description | React hooks, Suspense, Server Components, React 19 patterns. For TypeScript see typescript-idioms. |
| paths | ["**/*.jsx","**/*.tsx"] |
React 19 rewards composition, hooks, and server-aware patterns. Idiomatic React = functional, performant, accessible. Prefer co-located features, custom hooks for logic reuse, and server state libraries over hand-rolled fetch logic.
Scope: This file covers React-specific coding idioms for components, hooks, state, routing, and forms. For TypeScript type system patterns, see
@.agents/skills/typescript-idioms/SKILL.md. For file and folder layout, seereferences/project-structure.md. For general frontend design, see@.agents/skills/frontend-design/SKILL.md.
Functional components only — no class components in new code.
Composition over inheritance:
// ✅ Compound components
<Card>
<Card.Header>{title}</Card.Header>
<Card.Body>{children}</Card.Body>
</Card>
Error boundaries for graceful failure — wrap feature subtrees to catch render errors.
Render props for flexible, headless composition:
<DataLoader url="/api/tasks">
{({ data, isLoading, error }) => {
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return <TaskList tasks={data} />;
}}
</DataLoader>
Props typing — always explicit:
// ✅ Typed props with defaults
interface TaskCardProps {
task: Task;
onComplete?: (taskId: string) => void;
variant?: 'compact' | 'expanded';
}
export function TaskCard({ task, onComplete, variant = 'compact' }: TaskCardProps) {
// ...
}
One concern per component — if a component exceeds ~100 JSX lines, extract a sub-component.
Custom hooks for reusable logic:
function useTask(id: string) {
const { data, error, isLoading } = useQuery({
queryKey: ['task', id],
queryFn: () => taskApi.getTask(id),
});
return { task: data, error, isLoading };
}
useMemo/useCallback only for measured performance issues — not by default.
useEffect cleanup — always return cleanup function for subscriptions:
useEffect(() => {
const controller = new AbortController();
fetchTasks(controller.signal).then(setTasks);
return () => controller.abort(); // ✅ Cleanup on unmount
}, []);
useRef for values that don't trigger re-renders:
// ✅ Timer ref — doesn't cause re-render
const timerRef = useRef<ReturnType<typeof setInterval>>();
useEffect(() => {
timerRef.current = setInterval(pollStatus, 5000);
return () => clearInterval(timerRef.current);
}, []);
use() hook — read resources, promises, and context directly in render:
// ✅ Read a promise during render (replaces useEffect + useState)
function TaskDetail({ taskPromise }: { taskPromise: Promise<Task> }) {
const task = use(taskPromise);
return <h1>{task.title}</h1>;
}
// ✅ Read context without useContext
function TaskActions() {
const theme = use(ThemeContext);
return <button className={theme.primaryBtn}>Save</button>;
}
useActionState for form actions (replaces useFormState):
// ✅ Server-aware form with pending state
async function createTask(_prev: State, formData: FormData) {
const result = await api.createTask(Object.fromEntries(formData));
return result.error ? { error: result.error } : { success: true };
}
function TaskForm() {
const [state, formAction, isPending] = useActionState(createTask, { error: null });
return (
<form action={formAction}>
<input name="title" required />
{state.error && <p className="error">{state.error}</p>}
<button disabled={isPending}>{isPending ? 'Saving…' : 'Create'}</button>
</form>
);
}
useOptimistic for instant UI feedback:
const [optimisticTasks, addOptimistic] = useOptimistic(
tasks,
(state, newTask: Task) => [...state, newTask],
);
// Call addOptimistic(tempTask) before await api.createTask(tempTask)
<form action={fn}> for progressive enhancement — works before JS loads (see useActionState example above).
React Hook Form + Zod for validated forms:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const taskSchema = z.object({
title: z.string().min(1, 'Title is required').max(200),
priority: z.enum(['low', 'medium', 'high']),
});
type TaskFormData = z.infer<typeof taskSchema>;
function TaskForm({ onSubmit }: { onSubmit: (data: TaskFormData) => Promise<void> }) {
const { register, handleSubmit, formState: { errors } } = useForm<TaskFormData>({
resolver: zodResolver(taskSchema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('title')} />
{errors.title && <p>{errors.title.message}</p>}
<button type="submit">Create</button>
</form>
);
}
Controlled vs uncontrolled decision:
register) for simple forms — better performance, less boilerplateController) when the UI must react to every keystroke (live previews, dependent fields)React Router 7 data patterns — loaders and actions:
// ✅ Route-level data loading
export async function loader({ params }: LoaderFunctionArgs) {
return taskApi.getTask(params.id!);
}
export function TaskPage() {
const task = useLoaderData<typeof loader>();
return <TaskDetail task={task} />;
}
TanStack Router for type-safe routes:
const taskRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/tasks/$taskId',
loader: ({ params }) => taskApi.getTask(params.taskId),
component: TaskPage,
});
Route-level code splitting — always lazy-load route components with React.lazy + Suspense (see Performance section).
Decision tree:
useState→useContext→ Zustand → TanStack Query (for server state)
useState), lift only when shared by siblings.// ✅ Server state managed by TanStack Query
function useTasks() {
return useQuery({
queryKey: ['tasks'],
queryFn: () => taskApi.getTasks(),
staleTime: 5 * 60 * 1000,
});
}
// ✅ features/task/store/task.store.ts — Zustand for UI-only state
import { create } from 'zustand';
interface TaskUIState {
selectedId: string | null;
filter: 'all' | 'active' | 'done';
selectTask: (id: string | null) => void;
setFilter: (f: TaskUIState['filter']) => void;
}
export const useTaskUIStore = create<TaskUIState>((set) => ({
selectedId: null,
filter: 'all',
selectTask: (id) => set({ selectedId: id }),
setFilter: (filter) => set({ filter }),
}));
// Usage — client UI state only; server data stays in TanStack Query
function TaskToolbar() {
const { filter, setFilter } = useTaskUIStore();
return <FilterBar value={filter} onChange={setFilter} />;
}
// ✅ features/task/api/task.api.ts — interface
export interface TaskAPI {
getTasks(): Promise<Task[]>;
createTask(data: CreateTaskDTO): Promise<Task>;
}
// ✅ features/task/api/task.api.backend.ts — production (implements TaskAPI with fetch)
// ✅ features/task/api/task.api.mock.ts — test (implements TaskAPI with in-memory data)
For universal error handling principles, see
.agents/rules/error-handling-principles.md.
Error boundaries for component tree errors — use react-error-boundary or a custom class component:
// ✅ Wrap feature subtrees, log in componentDidCatch
<ErrorBoundary fallback={<ErrorMessage />}>
<TaskList />
</ErrorBoundary>
TanStack Query — use retry, isError, and error from query result (see State Management).
Log errors in componentDidCatch with correlationId and componentStack — never swallow silently.
React.memo only when profiling shows unnecessary re-renders.React.lazy + Suspense for route-level splitting:
import { lazy, Suspense } from 'react';
const TaskPage = lazy(() => import('./features/task/TaskPage'));
const ProfilePage = lazy(() => import('./features/profile/ProfilePage'));
function AppRoutes() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/tasks" element={<TaskPage />} />
<Route path="/profile" element={<ProfilePage />} />
</Routes>
</Suspense>
);
}
loading="lazy" and srcSet for responsive images.useMemo.useEffect for data fetching — use TanStack Query, SWR, or loaderskey={index} on dynamic lists — use stable, unique identifiersuseMemo/useCallback on everything — premature optimization// ❌ Unnecessary state
const [filteredTasks, setFilteredTasks] = useState<Task[]>([]);
useEffect(() => {
setFilteredTasks(tasks.filter(t => t.status === filter));
}, [tasks, filter]);
// ✅ Computed during render — no extra state
const filteredTasks = tasks.filter(t => t.status === filter);
useFormState — replaced by useActionState in React 19For universal testing principles, see
.agents/rules/testing-strategy.md. Below: React-specific patterns only.
React Testing Library + Vitest/Jest. Test behavior, not implementation.
Component rendering and interaction:
import { render, screen, fireEvent } from '@testing-library/react';
test('displays task title', () => {
render(<TaskCard task={mockTask} />);
expect(screen.getByText('Deploy fix')).toBeInTheDocument();
});
test('calls onComplete when button clicked', async () => {
const onComplete = vi.fn();
render(<TaskCard task={mockTask} onComplete={onComplete} />);
await fireEvent.click(screen.getByRole('button', { name: /complete/i }));
expect(onComplete).toHaveBeenCalledWith(mockTask.id);
});
Provider wrapper for tests — wrap components that depend on providers:
function createTestWrapper() {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
render(<TaskList />, { wrapper: createTestWrapper() });
Testing custom hooks with renderHook:
import { renderHook, waitFor } from '@testing-library/react';
test('useTask returns task data', async () => {
const { result } = renderHook(() => useTask('1'), {
wrapper: createTestWrapper(),
});
await waitFor(() => expect(result.current.task).toBeDefined());
expect(result.current.task?.title).toBe('Deploy fix');
});
MSW for API mocking — intercept at the network level:
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/tasks', () =>
HttpResponse.json([{ id: '1', title: 'Deploy fix', status: 'todo' }])
),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
| Tool | Purpose | Command |
|---|---|---|
| Prettier | Formatting | npx prettier --write . |
| ESLint + eslint-plugin-react-hooks | Linting | npx eslint . |
| TypeScript | Type checking | npx tsc --noEmit |