LLMSkillHub
mirroredCopied from irahardianto/awesome-agv under MIT. irahardianto has not claimed this page, and is not affiliated with LLM SkillHub.

irahardianto / react-idioms

React 19+ patterns: custom hooks, Suspense boundaries, state management, component composition, and web performance. Use when developing or refactoring React components and client applications. Pair with typescript-idioms.

0
SKILL.md
14.9 KB

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.

SituationReference to Load
Starting a React (Vite) project or reviewing file layoutreferences/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

  1. Functional components only — no class components in new code.
  2. Composition over inheritance:
    `tsx
    // ✅ Compound components
    <Card>
    <Card.Header>{title}</Card.Header>
    <Card.Body>{children}</Card.Body>
    </Card>
    `

  3. Error boundaries for graceful failure — wrap feature subtrees to catch render errors.
  4. Render props for flexible, headless composition:
    `tsx
    <DataLoader url="/api/tasks">
    {({ data, isLoading, error }) => {
    if (isLoading) return <Skeleton />;
    if (error) return <ErrorMessage error={error} />;
    return <TaskList tasks={data} />;
    }}
    </DataLoader>
    `

  5. Props typing — always explicit:
    `tsx
    // ✅ Typed props with defaults
    interface TaskCardProps {
    task: Task;
    onComplete?: (taskId: string) => void;
    variant?: 'compact' | 'expanded';
    }

    export function TaskCard({ task, onComplete, variant = 'compact' }: TaskCardProps) {
    // ...
    }
    `

  6. One concern per component — if a component exceeds ~100 JSX lines, extract a sub-component.

Hooks

  1. Custom hooks for reusable logic:
    `tsx
    function useTask(id: string) {
    const { data, error, isLoading } = useQuery({
    queryKey: ['task', id],
    queryFn: () => taskApi.getTask(id),
    });
    return { task: data, error, isLoading };
    }
    `

  2. useMemo/useCallback only for measured performance issues — not by default.
  3. useEffect cleanup — always return cleanup function for subscriptions:
    `tsx
    useEffect(() => {
    const controller = new AbortController();

    fetchTasks(controller.signal).then(setTasks);

    return () => controller.abort(); // ✅ Cleanup on unmount
    }, []);
    `

  4. useRef for values that don't trigger re-renders:
    `tsx
    // ✅ Timer ref — doesn't cause re-render
    const timerRef = useRef<ReturnType<typeof setInterval>>();

    useEffect(() => {
    timerRef.current = setInterval(pollStatus, 5000);
    return () => clearInterval(timerRef.current);
    }, []);
    `


React 19 Patterns

  1. use() hook — read resources, promises, and context directly in render:
    `tsx
    // ✅ 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>;
    }
    `

  2. useActionState for form actions (replaces useFormState):
    `tsx
    // ✅ 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>
    );
    }
    `

  3. useOptimistic for instant UI feedback:
    `tsx
    const [optimisticTasks, addOptimistic] = useOptimistic(
    tasks,
    (state, newTask: Task) => [...state, newTask],
    );
    // Call addOptimistic(tempTask) before await api.createTask(tempTask)
    `

  4. <form action={fn}> for progressive enhancement — works before JS loads (see useActionState example above).

Form Handling

  1. React Hook Form + Zod for validated forms:
    `tsx
    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>
    );
    }
    `

  2. Controlled vs uncontrolled decision:

    • Use uncontrolled (register) for simple forms — better performance, less boilerplate
    • Use controlled (Controller) when the UI must react to every keystroke (live previews, dependent fields)

Routing

  1. React Router 7 data patterns — loaders and actions:
    `tsx
    // ✅ 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} />;
    }
    `

  2. TanStack Router for type-safe routes:
    `tsx
    const taskRoute = createRoute({
    getParentRoute: () => rootRoute,
    path: '/tasks/$taskId',
    loader: ({ params }) => taskApi.getTask(params.taskId),
    component: TaskPage,
    });
    `

  3. 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)

  1. Local state first (useState), lift only when shared by siblings.
  2. Server state: TanStack Query — never in global state:
    `tsx
    // ✅ Server state managed by TanStack Query
    function useTasks() {
    return useQuery({
    queryKey: ['tasks'],
    queryFn: () => taskApi.getTasks(),
    staleTime: 5 * 60 * 1000,
    });
    }
    `

  3. Client state: Context for small/infrequent updates, Zustand/Jotai for complex/frequent:
    `tsx
    // ✅ 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} />;
    }
    `

  4. I/O isolation — abstract API behind an interface for testability:
    `tsx
    // ✅ 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)
    `


Error Handling

For universal error handling principles, see .agents/rules/error-handling-principles.md.

  1. Error boundaries for component tree errors — use react-error-boundary or a custom class component:
    `tsx
    // ✅ Wrap feature subtrees, log in componentDidCatch
    <ErrorBoundary fallback={<ErrorMessage />}>
    <TaskList />
    </ErrorBoundary>
    `

  2. TanStack Query — use retry, isError, and error from query result (see State Management).
  3. Log errors in componentDidCatch with correlationId and componentStack — never swallow silently.

Performance

  1. React.memo only when profiling shows unnecessary re-renders.
  2. Code splitting: React.lazy + Suspense for route-level splitting:
    `tsx
    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>
    );
    }
    `

  3. Virtual scrolling for long lists (TanStack Virtual).
  4. Image optimization — use loading="lazy" and srcSet for responsive images.
  5. Avoid inline object/array literals in props if causing re-render issues — hoist or useMemo.

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:

    // ❌ 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);
  • ❌ 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.

  1. Component rendering and interaction:
    `tsx
    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);
    });
    `

  2. Provider wrapper for tests — wrap components that depend on providers:
    `tsx
    function createTestWrapper() {
    const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
    return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    );
    }

    render(<TaskList />, { wrapper: createTestWrapper() });
    `

  3. Testing custom hooks with renderHook:
    `tsx
    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');
    });
    `

  4. MSW for API mocking — intercept at the network level:
    `tsx
    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());
    `


Formatting and Static Analysis

ToolPurposeCommand
PrettierFormattingnpx prettier --write .
ESLint + eslint-plugin-react-hooksLintingnpx eslint .
TypeScriptType checkingnpx 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