Install any skill in seconds. Free to start, no credit card required.
Get Started Free →React 18/19 patterns including hooks discipline, server/client component boundaries, Suspense + error boundaries, form actions, data fetching, state management decision trees, and accessibility-first composition. Use when writing or reviewing React components.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 153% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 115% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 95% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 117% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 85% | 0% |
Idiomatic React 18/19 patterns for building robust, accessible, performant component trees.
forwardRef/useEffect-heavy codetsx// Good: derive during render function Cart({ items }: { items: CartItem[] }) { const total = items.reduce((sum, i) => sum + i.price * i.qty, 0); return <span>{formatMoney(total)}</span>; } // Bad: derived state stored separately function Cart({ items }: { items: CartItem[] }) { const [total, setTotal] = useState(0); useEffect(() => { setTotal(items.reduce((sum, i) => sum + i.price * i.qty, 0)); }, [items]); return <span>{formatMoney(total)}</span>; }
Derived state in useEffect adds a render cycle, can desync, and obscures the data flow.
Effects, mutations, network calls, and subscriptions live in event handlers or useEffect — never in the render body.
React has no inheritance model for components. Compose with children, render props, or component props.
See rules/react/hooks.md for the full ruleset. Highlights:
setX(prev => prev + 1)) when new state depends on olduseMemo/useCallback only when a profiler or a dependency chain proves it mattersUsed by one component?
-> useState inside it
Used by parent + a few descendants?
-> lift to nearest common ancestor
Used across distant branches AND low-frequency reads (theme, auth, locale)?
-> React Context
High-frequency updates shared across the tree?
-> external store (Zustand, Jotai, Redux Toolkit)
Derived from a server?
-> server-state library (TanStack Query, SWR, RSC fetch)Most pages do not need context or a global store. Resist abstraction until duplicated lifting becomes painful.
tsx// Server Component - default, async, never ships JS for itself export default async function ProductPage({ params }: { params: { id: string } }) { const product = await db.product.findUnique({ where: { id: params.id } }); if (!product) notFound(); return <ProductView product={product} />; } // Client Component - opt in with "use client" "use client"; export function AddToCartButton({ productId }: { productId: string }) { const [pending, startTransition] = useTransition(); return ( <button disabled={pending} onClick={() => startTransition(() => addToCart(productId))} > {pending ? "Adding..." : "Add to cart"} </button> ); }
Boundaries:
children<form action={...}> or imperatively from event handlersimport a Server Component from a Client Component file — compose them via children insteadtsx<ErrorBoundary fallback={<ErrorView />}> <Suspense fallback={<UserSkeleton />}> <UserDetail id={id} /> </Suspense> </ErrorBoundary>
react-error-boundary for a hook-friendly wrapper> React 19+: This example uses useActionState. For React 18, use useFormState from react-dom instead.
tsx"use client"; import { useActionState } from "react"; const initial = { error: null as string | null }; async function updateUserAction(_prev: typeof initial, formData: FormData) { "use server"; const parsed = UserSchema.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { error: "Invalid input" }; await db.user.update({ where: { id: parsed.data.id }, data: parsed.data }); return { error: null }; } export function UserForm() { const [state, formAction, pending] = useActionState(updateUserAction, initial); return ( <form action={formAction}> <input name="name" required /> <button type="submit" disabled={pending}>Save</button> {state.error && <p role="alert">{state.error}</p>} </form> ); }
Use controlled when the value drives other UI, formats on every keystroke, or implements real-time validation.
For multi-step forms, dynamic field arrays, or cross-field validation: use a library (React Hook Form, TanStack Form). Roll-your-own state management for forms past trivial complexity is a maintenance trap.
| Need | Tool | |---|---| | Per-request data in Next.js App Router | RSC await fetch() | | Client-side cache + mutations + invalidation | TanStack Query | | Lightweight client cache + revalidation | SWR | | Real-time subscriptions | Server-Sent Events, WebSockets, or the lib's subscription API | | One-off fire-and-forget | fetch() in an event handler |
Avoid useEffect + fetch for application data — race conditions, no cache, no retry, no Suspense integration.
childrentsx<Layout> <Header /> <Main>{content}</Main> </Layout>
tsx<Page header={<Nav />} sidebar={<Filters />}> <Results /> </Page>
tsx<Tabs defaultValue="profile"> <Tabs.List> <Tabs.Trigger value="profile">Profile</Tabs.Trigger> <Tabs.Trigger value="settings">Settings</Tabs.Trigger> </Tabs.List> <Tabs.Panel value="profile"><Profile /></Tabs.Panel> <Tabs.Panel value="settings"><Settings /></Tabs.Panel> </Tabs>
Useful when the parent needs to pass parameters to the rendered output:
tsx<DataLoader id={id}> {({ data, isLoading }) => isLoading ? <Spinner /> : <UserCard user={data} />} </DataLoader>
Modern alternative: a hook (useData(id)) returning the same shape — usually cleaner.
React.memo Actually HelpsWrap a component in React.memo only when:
React.memo adds an equality check on every render. If props differ on most renders, the check is pure overhead.
themeContext does not re-render auth consumersuseSyncExternalStore for external state libraries — required for safe concurrent renderingkey props (database id, not array index)@tanstack/react-virtual or react-window once visible item count exceeds ~50 with non-trivial rows<button>, <a>, <nav>, <main>) before reaching for role attributes<label htmlFor> or aria-label if visually labeled by an iconaxe in component tests (see skills/react-testing)This skill is router-agnostic. The patterns above work with React Router, TanStack Router, Next.js App Router, Remix Router. Router-specific patterns (loaders, actions, nested layouts) follow the router's documentation — those are framework concerns layered on top of React core.
react-native-patterns skill (not present yet)react-reviewer for code review, react-build-resolver for build/bundler errors/react-review, /react-build, /react-testtsxfunction useDebounce<T>(value: T, delay = 300): T { const [debounced, setDebounced] = useState(value); useEffect(() => { const id = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(id); }, [value, delay]); return debounced; } function SearchBox() { const [query, setQuery] = useState(""); const debounced = useDebounce(query, 300); const { data } = useQuery({ queryKey: ["search", debounced], queryFn: () => searchApi(debounced), enabled: debounced.length > 0, }); return ( <> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <Results items={data ?? []} /> </> ); }
useOptimistictsx"use client"; import { useOptimistic } from "react"; export function MessageList({ messages }: { messages: Message[] }) { const [optimistic, addOptimistic] = useOptimistic( messages, (state, newMessage: Message) => [...state, newMessage], ); async function send(formData: FormData) { const text = String(formData.get("text")); addOptimistic({ id: "pending", text, sender: "me" }); await saveMessage(text); } return ( <> <ul>{optimistic.map((m) => <li key={m.id}>{m.text}</li>)}</ul> <form action={send}> <input name="text" /> <button type="submit">Send</button> </form> </> ); }
tsx// Two contexts: one rarely changes, one frequently const ThemeContext = createContext<Theme>("light"); const NotificationsContext = createContext<Notification[]>([]); // A component that only consumes ThemeContext does NOT re-render when notifications change
Other measured skills in the registry, with their headline benchmark lift.