---
name: react-development
description: Write production-quality React code day to day — components, hooks, props and TypeScript typing, forms, data fetching with TanStack Query or Server Components, effects done right, styling, accessibility, error and loading states, and idiomatic React 19 features (actions, use, ref as prop, React Compiler). Use this whenever the user asks to build, implement, refactor, or fix a React component, hook, page, form, modal, table, or feature; asks "how do I do X in React"; pastes React code with a bug; or asks about useEffect, custom hooks, controlled inputs, keys, context, Suspense, or Next.js client/server components. Also use it when generating any new React code, even if the request is small.
metadata:
  technology: React
  type: development
---

# React Development

This is the implementation layer: how a senior engineer writes a component, a hook, or a feature so it is correct the first time and boring to review. Architecture decisions live in `react-architecture`; this skill assumes those are made.

## 1. Before writing a component

Answer in one line each (in your head or in the reply):
1. **What state does it own, and what does it receive?** Owned state → `useState`; server state → query hook; URL state → router.
2. **Server or client?** (Next.js) Default server; add `"use client"` only if it uses state, effects, browser APIs or event handlers.
3. **What are its states?** loading, empty, error, partial, success, disabled — each gets rendered UI, not a `null`.
4. **What's its accessible role?** A clickable div is never the answer.

## 2. Component template

```tsx
import { type ComponentPropsWithoutRef } from 'react';

type OrderRowProps = {
  order: Order;
  onSelect: (id: Order['id']) => void;
  selected?: boolean;
} & Omit<ComponentPropsWithoutRef<'li'>, 'onSelect'>;

export function OrderRow({ order, onSelect, selected = false, ...rest }: OrderRowProps) {
  const total = formatMoney(order.total, order.currency);   // derive during render, don't store

  return (
    <li aria-selected={selected} {...rest}>
      <button type="button" onClick={() => onSelect(order.id)} aria-label={`Open order ${order.number}`}>
        <span>{order.number}</span>
        <span>{total}</span>
      </button>
    </li>
  );
}
```

Conventions:
- **Function declaration, named export, file = component name.** No `React.FC`, no default exports.
- **Props type exported** when other code composes it; spread `...rest` onto the root element so consumers can pass `className`, `data-*`, `aria-*`.
- **Destructure props with defaults** in the signature; no `props.x` in the body.
- **Derive, don't store.** Anything computable from props/state is computed in render (or `useMemo` if expensive). Storing derived values in state + syncing with effects is the #1 React bug source.
- **Early returns for states**: `if (isPending) return <Skeleton/>`; keep the happy path unindented at the bottom.
- **Keys** from stable ids. Index keys only for static, never-reordered lists.
- **Event handlers** named `handleX` inside, props named `onX`. Handlers that need no closure state can be defined outside the component.
- **Children over render props**; render props over `renderX` props.
- **Conditional classes** with `clsx`/`cn`; no string concatenation.
- Component > ~150 lines or > 3 pieces of state → extract a child or a hook.

## 3. Hooks

Rules of thumb:
- **Custom hook when logic is reused or a component's effect+state cluster has a name** (`useDebouncedValue`, `useClickOutside`, `useOrderFilters`). Name it after what it *returns*, not how it works.
- Return an object for > 2 values, a tuple for `[value, setter]` pairs.
- Hooks in `features/<f>/hooks/` when feature-specific; `shared/hooks/` when generic and framework-agnostic.

`useEffect` — the checklist before writing one:
| You want to… | Use instead |
|---|---|
| Compute something from props/state | Derive in render / `useMemo` |
| Reset state when a prop changes | `key` on the component, or derive |
| Respond to a user event | The event handler |
| Fetch data | TanStack Query / loader / Server Component |
| Notify parent of state change | Call `onChange` in the handler |
| Subscribe to an external store | `useSyncExternalStore` |
| Sync with a browser API / third-party widget / timer | **`useEffect`** — this is what it's for; return a cleanup |

When an effect is legitimate: one concern per effect, complete dependency array (don't silence the lint rule — restructure), cleanup for every subscription/timer/AbortController, and `useEffectEvent` (React 19) for reading latest values without re-subscribing.

`useRef` for DOM handles and mutable values that must not trigger renders (timers, previous value). React 19: `ref` is a normal prop — no `forwardRef` needed in new code.

## 4. Data fetching

**Client (TanStack Query):**
```tsx
export function useOrders(filters: OrderFilters) {
  return useQuery({
    queryKey: orderKeys.list(filters),
    queryFn: ({ signal }) => api.orders.list(filters, { signal }),
    staleTime: 30_000,
    placeholderData: keepPreviousData,
  });
}
```
- One hook per query in `features/<f>/api/`; components never call `useQuery` inline.
- Mutations: `useMutation` with `onSuccess: () => queryClient.invalidateQueries({ queryKey: orderKeys.all })`; optimistic updates only where latency is user-visible and rollback is implemented.
- Pass `signal` to fetch for cancellation.
- Handle `isPending`, `isError` (with `error` typed), and empty `data`.

**Server (Next.js RSC):** `async function Page()` awaits data directly; wrap slow sections in `<Suspense fallback>`; pass plain serialisable props to client components; mutations via Server Actions with `useActionState` and `revalidateTag`.

**React 19 `use()`**: read a promise passed from a server component inside a client component wrapped in Suspense — for streaming, not for ad-hoc fetching.

## 5. Forms

- **React Hook Form + Zod** (`zodResolver`). Schema is the single source of validation truth; infer the TS type from it.
- Inputs **uncontrolled** via `register`; controlled `Controller` only for custom components.
- Field errors rendered next to the field with `aria-invalid` and `aria-describedby`; summary `role="alert"` on submit failure.
- Submit: disable button while `isSubmitting`, catch server errors, map them to fields with `setError`, reset on success when appropriate.
- Server Actions (Next.js): `useActionState` for progressive enhancement; still validate with the same Zod schema on the server.
- Never `e.target.value` into `useState` for each field of a real form — that's what RHF replaces.

## 6. Context

- For **dependency injection and rarely-changing values** (theme, auth session, feature flags, a store instance).
- Provide a `useX()` hook that throws if used outside the provider.
- Memoise the value object; split contexts by change frequency.
- Not for frequently-changing state — use Zustand/Jotai selectors so consumers subscribe to slices.

## 7. Styling

- Tailwind: utilities in JSX, extract with `cva` for variant-heavy components, `cn()` to merge; no `@apply` soup.
- CSS Modules: one module per component, class names camelCase, tokens via CSS variables.
- Never inline `style={{}}` for static styles; only for truly dynamic values (computed widths, CSS vars).
- Respect `prefers-reduced-motion`; animate with CSS/`motion` not JS timers.

## 8. Accessibility defaults (every component)

- Semantic elements: `<button>`, `<a href>`, `<nav>`, `<main>`, `<ul>`; `<div onClick>` is a defect.
- Every input labelled; icon buttons get `aria-label`; images `alt`.
- Focus is visible and managed (modals trap and restore focus; use Radix/React Aria primitives rather than hand-rolling).
- Async feedback in `role="status"`/`role="alert"`.
- Colour contrast ≥ 4.5:1; hit targets ≥ 24 px.

## 9. Errors, loading, and edge cases

- Route/segment-level `ErrorBoundary` with a retry action; component-level boundaries around third-party widgets.
- Skeletons match final layout dimensions (prevents CLS); spinners only for < 300 ms actions.
- Empty states have copy and a primary action.
- Guard against: `undefined` data on first render, race conditions (stale response → cancelled via `signal` or ignored via query keys), double submit, unmounted `setState` (AbortController cleanup).
- Log unexpected errors to the monitoring client; never `console.error` and swallow.

## 10. TypeScript idioms

- Infer where possible; annotate function boundaries and exported types.
- `satisfies` for config; `as const` for literal unions; discriminated unions for component variants.
- Event types: `React.ChangeEvent<HTMLInputElement>`, `React.MouseEvent<HTMLButtonElement>`.
- Generic components: `function Table<T extends { id: string }>(props: TableProps<T>)` — no `FC`.
- No `any`; `unknown` + narrowing at boundaries; Zod-parse untrusted JSON.

## 11. Definition of done for a component

- [ ] All UI states rendered (loading/empty/error/success)
- [ ] Keyboard-operable, labelled, roles correct
- [ ] No derived state stored; no effect that could be a computation or handler
- [ ] Types exported, no `any`
- [ ] Story or test covering the main states (see `react-testing`)
- [ ] Under ~150 lines or split with reason

## Anti-patterns to fix on sight

- `useEffect(() => setX(f(props)), [props])` — derive instead.
- `useEffect` fetch with `useState` loading flags.
- Mutating state (`arr.push` then `setArr(arr)`).
- Index keys on dynamic lists; `Math.random()` keys.
- `useCallback`/`useMemo` everywhere with no memoised consumer.
- `forwardRef` in new React 19 code; `defaultProps`; class components.
- `dangerouslySetInnerHTML` with unsanitised content.
- Prop drilling a setter four levels down.
- Business logic (pricing, permissions) inside JSX.
