Frontend No. 060

React Patterns skill for Claude Code

Fewer effects, derived state, components that compose. Makes Claude write React the way the current docs intend: state kept minimal, values derived during render, user-driven logic in event handlers, and effects reserved for syncing with outside systems with proper cleanup. It prefers composition through children over boolean-prop sprawl, uses React 19 Actions, ref-as-prop and useEffectEvent, and reviews diffs with a numbered findings list.

  • namereact-patterns
  • size234 lines · ≈2,600 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Writes and reviews modern React 19 components with disciplined hooks, composition over configuration, minimal effects, stable keys, and scoped context. Covers derived state, event handlers vs. effects, useEffectEvent, Actions with useActionState and useOptimistic, ref as a prop, and React Compiler. Use when building or refactoring React components, when a useEffect syncs state, when props drill through many layers, when lists re-mount, or when reviewing a .tsx/.jsx diff.

Try asking

  • This component has four useEffects that keep state in sync — clean it up
  • Refactor this Card so it stops taking twelve boolean props
  • Review my React diff for hooks mistakes and key problems

Use it when

  • A useEffect copies props or state into other state
  • Props are drilled through three or more layers
  • List items lose input focus or state when the list changes
  • You are migrating forwardRef, Context.Provider or form handlers to React 19 APIs

Skip it when

  • The problem is measured render slowness — use react-rerenders instead
  • You are deciding between URL, server cache and global store — use state-management

How to check the result

Type-check and hooks lint pass with no new warnings, and a Testing Library test of the user flow behaves the same before and after the refactor under StrictMode.

The common mistake it prevents

Silencing exhaustive-deps to stop an effect re-running, which leaves the effect reading stale props instead of fixing why the dependency changes.

~/.claude/skills/react-patterns/SKILL.md

--- name: react-patterns description: Writes and reviews modern React 19 components with disciplined hooks, composition over configuration, minimal effects, stable keys, and… license: MIT ---

Good React code renders from the minimum state, computes everything else during render, runs side effects only to synchronize with systems outside React, and composes small components through children rather than piling on boolean props.

Before changing anything

  1. Check the React version and tooling: npm ls react react-dom and whether babel-plugin-react-compiler / eslint-plugin-react-hooks are configured. React 19 APIs (use, useActionState, ref-as-prop) need react@19; useEffectEvent and <Activity> need 19.2+.
  2. Check whether the project is on a framework (Next.js, React Router, Remix, Vite SPA). Server Components and Actions behave differently per framework — see react-server-components for the server side.
  3. Run the existing lint: npx eslint . --ext .ts,.tsx. Treat every react-hooks/* warning as a bug report, not noise.

Workflow

- [ ] 1. Identify the minimal state (what cannot be computed from something else)
- [ ] 2. Move derivable values into render
- [ ] 3. Move user-caused side effects into event handlers
- [ ] 4. Keep effects only for external synchronization, with cleanup
- [ ] 5. Fix component boundaries: composition, children, slots
- [ ] 6. Fix list keys and identity-driven resets
- [ ] 7. Lint, type-check, test interaction, check StrictMode double-run

Effects discipline

An effect is an escape hatch for syncing with something React does not control (network subscription, DOM API, third-party widget, timer). Run through this table before writing useEffect:

You want to…Do this instead of an effect
Compute a value from props/stateCompute it in render; useMemo only if measured slow (and not under React Compiler)
Reset all state when a prop changesGive the component key={id} from the parent
Adjust one piece of state when a prop changesStore the previous prop and compare during render, or restructure so it is derived
Respond to a click/submitPut the logic in the event handler
Notify a parent of a changeCall the parent’s callback in the same handler that changes state
Fetch dataFramework loader / Server Component / TanStack Query; raw effect fetch only with abort + race handling
Subscribe to an external storeuseSyncExternalStore
Read the latest prop inside an effect without re-running ituseEffectEvent (19.2+)

When an effect is justified:

useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/rooms/${roomId}`, { signal: controller.signal })
    .then((r) => r.json())
    .then(setRoom)
    .catch((e) => { if (e.name !== "AbortError") setError(e); });
  return () => controller.abort();   // cleanup prevents stale responses winning
}, [roomId]);
// Latest-value read without re-subscribing (React 19.2+)
const onMessage = useEffectEvent((msg: Message) => {
  if (!muted) showToast(msg.text);
});
useEffect(() => {
  const conn = connect(roomId);
  conn.on("message", onMessage);
  return () => conn.disconnect();
}, [roomId]); // onMessage is intentionally not a dependency

Rules:

  • Never suppress react-hooks/exhaustive-deps. If a dependency causes re-runs, the fix is to move the value inside the effect, derive it, or use useEffectEvent.
  • Every subscription, listener, timer, or observer must return a cleanup.
  • StrictMode mounts, unmounts, and re-mounts in development. If that breaks behavior, the cleanup is wrong — do not remove StrictMode.
  • Chains of effects that set state that triggers another effect are a red flag; collapse them into one event handler or one derived computation.

State design

  • Keep state minimal and non-redundant. If fullName can be built from first and last, it is not state.
  • Never mirror props into state (useState(props.value)) unless the prop is an explicit initial value; name it initialValue to say so.
  • Group values that change together into one object or a reducer; split values that change independently.
  • Use useReducer when the next state depends on the previous state through more than two or three transitions, or when several event handlers mutate the same state.
  • Store IDs, not copies of objects: selectedId plus a lookup, never selectedItem that goes stale when the list updates.
  • Where state lives (URL, server cache, global store) is decided in state-management; this skill covers component-local design.

Composition over configuration

Prefer children and slots to boolean and render-mode props.

// Avoid: <Card title="..." showFooter footerAlign="right" variant="compact" icon={...} />
// Prefer composition:
<Card>
  <Card.Header>
    <Card.Title>Invoices</Card.Title>
  </Card.Header>
  <Card.Body>{rows}</Card.Body>
  <Card.Footer align="end"><Button>Export</Button></Card.Footer>
</Card>
  • Pass components as children to avoid prop drilling: the parent that owns data renders <Layout sidebar={<UserMenu user={user} />}>, so Layout never sees user.
  • More than ~5 boolean props or any prop combination that is invalid together means split the component or use a discriminated-union prop type.
  • Custom hooks extract reusable stateful logic, not JSX. Name them useX, keep them pure of rendering, and return a stable, small API.

Context

  • Use context for values that many distant components read and that change rarely: theme, locale, current user, a feature’s store handle.
  • Every consumer re-renders when the context value changes. Split fast-changing values (e.g. cursor position) from slow ones into separate contexts.
  • Memoize object values given to a provider unless React Compiler is on: const value = useMemo(() => ({ user, logout }), [user, logout]).
  • In React 19, render the context directly as provider: <ThemeContext value={theme}>. use(ThemeContext) may be called conditionally, unlike useContext.
  • Wrap access in a hook that throws outside the provider:
export function useCart() {
  const ctx = use(CartContext);
  if (!ctx) throw new Error("useCart must be used inside <CartProvider>");
  return ctx;
}

Keys and identity

  • Keys must be stable and unique among siblings: database IDs, slugs. Never Math.random() or crypto.randomUUID() in render; array index only for static lists that never reorder, insert, or filter.
  • Changing a key is the sanctioned way to reset a subtree’s state: <ProfileForm key={userId} />.
  • Never define a component inside another component’s body; it gets a new identity each render and remounts, losing state and focus.

React 19 APIs to prefer

NeedAPI
Form submit with pending + error stateuseActionState(action, initial) returning [state, formAction, isPending]
Show submit button pending inside a formuseFormStatus() from react-dom (must be in a child of <form>)
Instant UI before server confirmsuseOptimistic(state, updateFn) inside a transition/action
Non-urgent update that may be interruptedstartTransition / useTransition
Read a promise or context in renderuse(promise) with a Suspense boundary; promise must be created outside render (cached)
Forward a refAccept ref as a normal prop; forwardRef is no longer needed
Clean up a ref callbackReturn a cleanup function from the ref callback
Keep hidden UI mounted with state<Activity mode="hidden"> (19.2+)
function RenameForm({ id, name }: { id: string; name: string }) {
  const [state, formAction, isPending] = useActionState(
    async (_prev: { error?: string }, formData: FormData) => {
      const res = await renameProject(id, String(formData.get("name")));
      return res.ok ? {} : { error: res.message };
    },
    {},
  );
  return (
    <form action={formAction}>
      <input name="name" defaultValue={name} aria-describedby="rename-error" />
      <button disabled={isPending}>{isPending ? "Saving…" : "Save"}</button>
      {state.error && <p id="rename-error" role="alert">{state.error}</p>}
    </form>
  );
}

Memoization and React Compiler

  • If React Compiler is enabled, do not add useMemo, useCallback, or memo by default; the compiler handles it. Keep existing ones unless they are wrong.
  • Without the compiler, memoize only when: a value is a dependency of an effect, a prop to a memo child, or a profiler shows a slow computation. Rendering performance investigations belong to react-rerenders.
  • The compiler skips components that break the Rules of React (mutating props, reading refs during render, conditional hooks). Lint errors from eslint-plugin-react-hooks tell you which.

Hard rules

  • Hooks only at the top level of components and custom hooks; never in conditions, loops, or after an early return.
  • Never mutate state or props. Copy: setItems((xs) => [...xs, item]).
  • Never read or write ref.current during render (except lazy init).
  • Components must be pure: same props and state, same JSX. Side effects go in handlers or effects.
  • Controlled inputs need value plus onChange; uncontrolled need defaultValue. Switching between them warns and loses input.
  • dangerouslySetInnerHTML only with sanitized HTML (DOMPurify or server-side sanitizer).

Verification

  1. npx tsc --noEmit and the hooks lint pass with zero new warnings.
  2. Interact with the component in development under StrictMode: no duplicated requests that matter, no console warnings about keys, controlled/uncontrolled, or act.
  3. For refactors that removed effects, a Testing Library test drives the same user flow before and after (see testing-library).
  4. If behavior involves fast prop changes (route params, search input), test rapid changes to prove stale responses cannot overwrite fresh ones.

Review output

When reviewing a React diff, report findings in this shape:

### React review — src/features/cart/CartPanel.tsx
1. **[bug]** L42 effect copies `items` into `total` state → derive `total` in render.
2. **[bug]** L77 list uses index key while items can be removed → use `item.id`.
3. **[smell]** 7 boolean props on `CartRow` → split into `CartRow` + `CartRow.Actions`.
4. **[ok]** Subscription effect has cleanup and correct deps.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc