Install
openclaw skills install 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 compound-eng-react-frontendVerify before implementing: For App Router patterns, React 19 APIs, or version-specific behavior, look up current docs via Context7 (query-docs) 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 automaticallyFrontend bugs that survive type-checking and unit tests usually land in one of five race classes. Hunt each one explicitly during review:
useEffect registered a listener/timer/observer without returning cleanup (see Effect rules above for the rule).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.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.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.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."
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 framework config (Next.js: reactCompiler: true in next.config). Non-framework: install 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.
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.
*.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: 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.
eslint-disable, @ts-ignore) in new codeuseEffect dependency arrays not manually overriddenforwardRef usage in React 19+ projects (use ref prop directly)