| name | react-idioms |
| description | React hooks, Suspense, Server Components, React 19 patterns. For TypeScript see typescript-idioms. |
| paths | ["**/*.jsx","**/*.tsx"] |
React Idioms and Patterns
Core Philosophy
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, see references/project-structure.md (and the shared @.agents/skills/frontend-design/references/frontend-layout.md). For general frontend design, see @.agents/skills/frontend-design/SKILL.md.
Loading guard: If the project uses Next.js (App Router — app/ dir or next.config.*), load @.agents/skills/nextjs-idioms/SKILL.md instead of this skill for App-Router-specific patterns. This skill still applies to client components and pure-React (Vite) SPAs.
When to Load References
Load these before writing code in the matching context — not after.
| Situation | Reference to Load |
|---|
| Starting a React (Vite) project or reviewing file layout | references/project-structure.md + @.agents/skills/frontend-design/references/frontend-layout.md |
| TypeScript type system, async, Zod, error types | @.agents/skills/typescript-idioms/SKILL.md (always co-load) |
| Zod schemas / boundary validation | @.agents/skills/typescript-idioms/references/zod-patterns.md |
| Async / I/O / coercion pitfalls | @.agents/skills/typescript-idioms/references/ts-patterns-and-anti-patterns.md |
| Next.js App Router (RSC, Server Actions, caching) | @.agents/skills/nextjs-idioms/SKILL.md (use that skill instead for Next projects) |
Component Patterns
-
Functional components only — no class components in new code.
-
Composition over inheritance:
<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:
interface TaskCardProps {
task: Task;
onComplete?: (taskId: ) => ;
?: | ;
}
() {
}
Hooks
-
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();
}, []);
-
useRef for values that don't trigger re-renders:
const timerRef = useRef<ReturnType<typeof setInterval>>();
useEffect(() => {
timerRef.current = setInterval(pollStatus, 5000);
return () => (timerRef.);
}, []);
React 19 Patterns
-
use() hook — read resources, promises, and context directly in render:
function TaskDetail({ taskPromise }: { taskPromise: Promise<Task> }) {
const task = use(taskPromise);
return <h1>{task.title}</h1>;
}
function TaskActions() {
const theme = use(ThemeContext);
return <button className={theme.primaryBtn}>Save</button>;
}
-
useActionState for form actions (replaces useFormState):
async function createTask(_prev: State, formData: FormData) {
const result = await api.createTask(Object.fromEntries(formData));
return result.error ? { error: result.error } : { : };
}
() {
[state, formAction, isPending] = (createTask, { : });
(
);
}
Form Handling
-
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}</>}
Create
);
}
Routing
-
React Router 7 data patterns — loaders and actions:
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).
State Management
Decision tree: useState → useContext → Zustand → TanStack Query (for server state)
- Local state first (
useState), lift only when shared by siblings.
- Server state: TanStack Query — never in global state:
function useTasks() {
return useQuery({
queryKey: ['tasks'],
queryFn: () => taskApi.getTasks(),
staleTime: 5 * 60 * 1000,
});
}
- Client state: Context for small/infrequent updates, Zustand/Jotai for complex/frequent:
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>(() => ({
: ,
: ,
: ({ : id }),
: ({ filter }),
}));
() {
{ filter, setFilter } = ();
;
}
Error Handling
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:
<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.
Performance
React.memo only when profiling shows unnecessary re-renders.
- Code splitting:
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>
);
}
- Virtual scrolling for long lists (TanStack Virtual).
- Image optimization — use
loading="lazy" and srcSet for responsive images.
Anti-Patterns
- ❌
useEffect for data fetching — use TanStack Query, SWR, or loaders
- ❌ Prop drilling through 3+ levels — use Context or state manager
- ❌
key={index} on dynamic lists — use stable, unique identifiers
- ❌
useMemo/useCallback on everything — premature optimization
- ❌ State for derived data — compute during render:
const [filteredTasks, setFilteredTasks] = useState<Task[]>([]);
useEffect(() => {
setFilteredTasks(tasks.filter(t => t.status === filter));
}, [tasks, filter]);
const filteredTasks = tasks.filter(t => t.status === filter);
- ❌ Direct DOM manipulation — use refs and React's render cycle
- ❌
useFormState — replaced by useActionState in React 19
- ❌ Global state for server data — use TanStack Query/SWR instead
Testing
For 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: } } });
(
);
}
(, { : () });
Formatting and Static Analysis
| Tool | Purpose | Command |
|---|
| Prettier | Formatting | npx prettier --write . |
| ESLint + eslint-plugin-react-hooks | Linting | npx eslint . |
| TypeScript | Type checking | npx tsc --noEmit |
Related
- Code Idioms and Conventions @.agents/rules/code-idioms-and-conventions.md
- TypeScript Idioms @.agents/skills/typescript-idioms/SKILL.md
- React Project Structure @.agents/skills/react-idioms/references/project-structure.md
- Frontend Layout (framework-neutral, shared with Vue) @.agents/skills/frontend-design/references/frontend-layout.md
- Frontend Design @.agents/skills/frontend-design/SKILL.md
- Next.js Idioms (for Next.js App Router projects) @.agents/skills/nextjs-idioms/SKILL.md
- Security Principles @.agents/rules/security-principles.md
- Accessibility Principles @.agents/rules/accessibility-principles.md
- Testing Strategy @.agents/rules/testing-strategy.md
- Error Handling Principles @.agents/rules/error-handling-principles.md
- Logging and Observability @.agents/rules/logging-and-observability-mandate.md
- Architectural Patterns @.agents/rules/architectural-pattern.md