Install
openclaw skills install @iliaal/compound-eng-react-frontendReact 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.
openclaw skills install @iliaal/compound-eng-react-frontendVerify before implementing: For App Router patterns, React 19 APIs, or version-specific behavior, look up current docs (Context7 query-docs if available, else the framework's official docs via web search) before writing code. Training data may lag current releases.
ComponentPropsWithoutRef<'button'>, add custom props via intersectionReact.ReactNode for children, React.ReactElement for single element, render prop (data: T) => ReactNode<T> with keyof T for column keys, T extends { id: string } for constraintsReact.MouseEvent<HTMLButtonElement>, FormEvent<HTMLFormElement>, ChangeEvent<HTMLInputElement>as const for custom hook tuple returnsuseRef<HTMLInputElement>(null) for DOM (use ?.), useRef<number>(0) for mutable valuesuseState<User | null>(null) for unions/null{ type: 'set'; payload: number } | { type: 'reset' }useX() hook if context is nullEffects 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 automaticallyFive race classes survive type-checking and unit tests -- hunt each one during review (cleanup/cancellation mechanics: Effect rules above):
| Class | Production signal | Fix |
|---|---|---|
| Lifecycle cleanup gap | "state update on unmounted component" warnings, leaks under rapid navigation | Return cleanup from every effect that registers a listener/timer/observer |
| Remount-timing mistake | Async callback mutates state/DOM after route change/unmount (fetch().then(setData) resolves post-navigation) | Cancel per the cancellation hierarchy |
| Boolean-as-state for non-binary UI | Contradictory combos (isLoading: true, error: Error) | State constant ('idle' | 'loading' | 'success' | 'error') + transition function; invalid states unreachable |
| Stale promise/timer, no cancel path | Promise chain or setTimeout holds setState after the component moved on | Bind every async op to a cancel mechanism; test the cleanup path |
| Per-element handlers on large lists | N closures/subscriptions per row, stale-closure bugs on rapid re-renders | Delegate: one parent handler + event.target.closest(...) when >~50 items or frequent updates |
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<State>()(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 rollbackCritical -- 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 ? <A /> : <B />), 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 reactCompiler: true in next.config (non-framework: babel-plugin-react-compiler). Keep components pure.
forwardRef deprecated. Accept ref?: React.Ref<HTMLElement> 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 <form action={...}>'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<Activity mode='visible'|'hidden'> -- preserves state/DOM for toggled components (experimental)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.
Server → client boundary: pass only the fields a client component actually uses, not whole ORM rows or fetch objects. Every prop crossing the 'use client' boundary is serialized into the payload, so a 50-field user object read for one field still ships all 50.
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:
React.cache() for per-request dedupgenerateStaticParams for static generationgenerateMetadata for dynamic SEOtitle: { default: 'App', template: '%s | App' } for cascading page titlesafter() for non-blocking side effects (logging, analytics) -- runs after response is sent*.test.tsx. Default for React components.renderHook + act, co-located *.test.tsgetByRole > getByLabelText > getByPlaceholderText > getByText > getByTestIdshould <behavior> when <condition>userEvent over fireEvent for realistic interactionsfindBy* for async elements, waitFor after state-triggering actionsvi.clearAllMocks() in beforeEach. Recreate state per test.
General testing discipline (anti-patterns, rationalization resistance): see ia-writing-tests skill.
See testing patterns and examples for component, hook, and mocking examples.
See e2e testing for Playwright patterns.For Tailwind v4 configuration, utility patterns, dark mode, and component variants, see ia-tailwind-css skill.
Class sorting in JSX: keep Tailwind classes in canonical order (enforce via eslint-plugin-better-tailwindcss).
eslint-disable, @ts-ignore) in new codeuseEffect dependency arrays not manually overriddenforwardRef usage in React 19+ projects (use ref prop directly)