ia-react-frontend
by @iliaal
React architecture patterns, TypeScript, Next.js, hooks, and testing. Use when working with React component structure, state management, Next.js routing, Vit...
clawhub install compound-eng-react-frontendπ About This Skill
name: ia-react-frontend class: language description: >- React architecture patterns, TypeScript, Next.js, hooks, and testing. Use when working with React component structure, state management, Next.js routing, Vitest, React Testing Library, or reviewing React code. For visual design and aesthetic direction, use frontend-design instead. paths: "/*.tsx,/*.jsx"
React Frontend
Verify before implementing: For App Router patterns, React 19 APIs, or version-specific behavior, search current docs via search_docs before writing code. Training data may lag current releases.
Component TypeScript
ComponentPropsWithoutRef<'button'>, add custom props via intersectionReact.ReactNode for children, React.ReactElement for single element, render prop (data: T) => ReactNode with keyof T for column keys, T extends { id: string } for constraintsReact.MouseEvent, FormEvent, ChangeEventas const for custom hook tuple returnsuseRef(null) for DOM (use ?.), useRef(0) for mutable valuesuseState(null) for unions/null{ type: 'set'; payload: number } | { type: 'reset' }useX() hook if context is nullEffects Decision Tree
Effects are escape hatches -- most logic should NOT use effects.
| Need | Solution |
|------|----------|
| Derived value from props/state | Calculate during render (useMemo if expensive) |
| Reset state on prop change | key prop on component |
| Respond to user event | Event handler |
| Notify parent of state change | Call onChange in event handler, or fully controlled component |
| Chain of state updates | Calculate all next state in one event handler |
| Sync with external system | Effect with cleanup |
Effect rules:
setItems(prev => [...prev, item])) to remove state dependenciesuseEffectEvent for non-reactive values (e.g., theme in a connection effect)AbortController for fetch; ignore flag for non-cancellable promises; React Query handles both automaticallyConcurrency & Race Classes
Frontend bugs that survive type-checking and unit tests usually land in one of five race classes. Hunt each one explicitly during review:
1. Lifecycle cleanup gaps -- in-production signal: "Can't perform state update on an unmounted component" warnings, slow-burn memory leaks under rapid navigation, duplicate event handlers firing. Root cause: useEffect registered a listener/timer/observer without returning cleanup (see Effect rules above for the rule).
2. Remount-timing mistakes -- async callbacks mutate DOM or state after swap / disconnect / route change. Classic cases: a fetch().then(setData) resolves after navigation to a different route; a requestAnimationFrame fires after the parent unmounts. See "Data fetching" in Effect rules above for the cancellation hierarchy.
3. Boolean-as-state for UI that isn't binary -- isLoading: boolean can't represent idle | loading | success | error | retry without creating inconsistent combinations (isLoading: true, error: Error is contradictory). Prefer an explicit state constant ('idle' | 'loading' | 'success' | 'error') with a transition function so invalid states are unreachable.
4. Stale promises and timers with no cancel path -- a promise chain or setTimeout holds a reference to setState after the component's moved on. Bind every async operation to a cancel mechanism per the cancellation hierarchy above, and verify the cleanup path is exercised by a test.
5. Per-element handlers where delegation would be safer -- attaching onClick to every row in a list creates N closures and N subscriptions; delegated listeners (single handler on the parent reading event.target.closest(...)) are safer under rapid re-renders, avoid stale-closure bugs, and scale to large lists. Use delegation when the list exceeds ~50 items or updates frequently.
These classes produce bugs that are intermittent, environment-dependent, and invisible to type-checking -- exactly the ones that reach production. Review for them deliberately, not just as "subscriptions need cleanup."
State Management
Local UI state β useState, useReducer
Shared client state β Zustand (simple) | Redux Toolkit (complex)
Atomic/granular β Jotai
Server/remote data β React Query (TanStack Query)
URL state β nuqs, router search params
Form state β React Hook Form
Key patterns:
create()(devtools(persist((set) => ({...})))) -- use slices for scale, selective subscriptions to prevent re-renders['users', 'detail', id] as const), staleTime/gcTime, optimistic updates with onMutate/onError rollbackPerformance
Critical -- eliminate waterfalls:
Promise.all() for independent async operationsawait into branches where actually neededCritical -- bundle size:
index.ts re-exports)next/dynamic or React.lazy() for heavy componentscontent-visibility: auto + contain-intrinsic-size on long lists -- skips off-screen layout/paintRe-render optimization:
state.items.length > 0 not state.items)setCount(c => c + 1)useState(() => expensiveComputation())useTransition for non-urgent updates (search filtering)useDeferredValue for expensive derived UIcondition ? : ), not && for conditionalsReact.memo only for expensive subtrees with stable propsReact Compiler (React 19): auto-memoizes -- write idiomatic React, remove manual useMemo/useCallback/memo. Enable via framework config (Next.js: reactCompiler: true in next.config). Non-framework: install babel-plugin-react-compiler. Keep components pure.
React 19
forwardRef deprecated. Accept ref?: React.Ref as regular propuseFormState: const [state, formAction, isPending] = useActionState(action, initialState)const [optimistic, addOptimistic] = useOptimistic(state, mergeFn) for instant UI feedbackconst { pending } = useFormStatus() in child of 'use server' directive. Validate inputs (Zod), revalidateTag/revalidatePath after mutations. Server Actions are public endpoints -- always verify auth/authz inside each action, not just in middleware or layout guards -- preserves state/DOM for toggled components (experimental)Next.js App Router
File conventions: page.tsx (route UI), layout.tsx (shared wrapper), template.tsx (re-mounted on navigation, unlike layout), loading.tsx (Suspense), error.tsx (error boundary), not-found.tsx (404), default.tsx (parallel route fallback), route.ts (API endpoint)
Rendering modes: Server Components (default) | Client ('use client') | Static (build) | Dynamic (request) | Streaming (progressive)
Decision: Server Component unless it needs hooks, event handlers, or browser APIs. Split: server parent + client child. Isolate interactive components as 'use client' leaf components -- keep server components static with no global state or event handlers.
Routing patterns:
(name) -- organize without affecting URL@slot -- independent loading states in same layout(.) -- modal overlays with full-page fallbackCaching:
fetch(url, { cache: 'force-cache' }) -- staticfetch(url, { next: { revalidate: 60 } }) -- ISRfetch(url, { cache: 'no-store' }) -- dynamicfetch(url, { next: { tags: ['products'] } }) then revalidateTag('products')Data fetching: Fetch in Server Components where data is used. Use Suspense boundaries for slow queries. React.cache() for per-request dedup. generateStaticParams for static generation. generateMetadata for dynamic SEO. Static metadata with title: { default: 'App', template: '%s | App' } for cascading page titles. after() for non-blocking side effects (logging, analytics) -- runs after response is sent. Hoist static I/O (fonts, config) to module level -- runs once, not per request.
Testing (Vitest + React Testing Library)
*.test.tsx. Default for React components.renderHook + act, co-located *.test.tsgetByRole > getByLabelText > getByPlaceholderText > getByText > getByTestIdshould when userEvent over fireEvent for realistic interactionsfindBy* for async elements, waitFor after state-triggering actionsvi.clearAllMocks() in beforeEach. Recreate state per test.Tailwind Integration
For Tailwind v4 configuration, utility patterns, dark mode, and component variants, see ia-tailwind-css skill.
Class sorting in JSX: when using clsx, cva, cn, tv, or tw utility functions, keep Tailwind classes in canonical order. Configure eslint-plugin-better-tailwindcss with useSortedClasses and functions: ["clsx", "cva", "cn", "tv", "tw"] to enforce this automatically across JSX attributes and helper calls.
Discipline
References
Verify
eslint-disable, @ts-ignore) in new codeuseEffect dependency arrays not manually overriddenforwardRef usage in React 19+ projects (use ref prop directly)