# React

> MUST USE when writing or editing React components, hooks, state management, effects, performance, loading/empty states, Zustand stores, or styling. Covers React 19.2 (use, Actions, useActionState, useOptimistic, useFormStatus, ref as prop, Context provider, Activity), the Rules of Hooks, React Compiler v1.0 memoization, component splitting and custom hooks, composition over boolean props, useEffect avoidance, re-render performance, list keys, loading skeletons and empty states, Zustand client-state stores, and Tailwind v4 design tokens.

- Skill: `spardutti/react` (Agent Skill, multi-file: 9 files)
- Install (CLI): `npx skillmds@latest add spardutti/react`
- Raw SKILL.md: https://api.skillmd.com/api/skills/spardutti/react/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Spardutti (https://skillmd.com/u/spardutti)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/spardutti/react

---


# React — Components, Hooks, State, Styling

Modern React component, state, effect, and styling patterns. Baseline is React 19.2 + React Compiler v1.0 (stable since October 2025) and Tailwind v4.

## Quick Reference — When to Load What

| Working on… | Read |
|---|---|
| **Starting a new app, page, or screen** | **SHADCN.md + TAILWIND-TOKENS.md first** — check what the repo already has |
| Splitting components, custom hooks, file size, prop drilling | COMPONENT-DESIGN.md |
| useEffect — whether you even need one, cleanup, dependency arrays | USE-EFFECT.md |
| Re-renders, memoization, React Compiler, list keys, `<Activity>` | PERFORMANCE.md |
| Loading skeletons, empty states, Suspense + error boundary pairing | LOADING-STATES.md |
| Client state stores — Zustand, selectors, middleware, slices | ZUSTAND.md |
| Styling — colors, spacing, design tokens, no arbitrary values | TAILWIND-TOKENS.md |
| Forms — React Hook Form, Zod, field errors, submission state | FORMS.md |
| shadcn components — Base UI vs Radix, cva variants, `cn()`, where files go | SHADCN.md |

## Critical Gotchas (Always-Inline)

These are the highest-leverage React 19 mistakes. Memorize them.

### 1. `ref` Is a Normal Prop — No `forwardRef`

```tsx
// BAD: forwardRef is deprecated in React 19
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
  <input ref={ref} {...props} />
));

// GOOD: ref is just a prop
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}
```

### 2. `<Context>` Is the Provider — No `.Provider`

```tsx
const ThemeContext = createContext<Theme>("light");

// BAD: .Provider is deprecated
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>

// GOOD: render the context itself
<ThemeContext value={theme}>{children}</ThemeContext>
```

### 3. `use()` Reads Context and Promises — Conditionally

Unlike `useContext`, `use()` works inside `if` blocks and loops, and unwraps promises with Suspense.

```tsx
import { use } from "react";

function Panel({ show }: { show: boolean }) {
  if (!show) return null;
  const theme = use(ThemeContext);     // legal inside a conditional
  return <hr className={theme} />;
}
```

### 4. Derive State During Render — Don't Sync It in an Effect

```tsx
// BAD: extra state + an Effect that lags one render behind
const [filtered, setFiltered] = useState(items);
useEffect(() => setFiltered(items.filter(i => i.active)), [items]);

// GOOD: compute during render — no state, no Effect
const filtered = items.filter(i => i.active);
```

If a value can be computed from props or other state, it is not state. See USE-EFFECT.md.

### 5. Never Hand-Roll Submission State — RHF by Default, Actions for the Simple Case

The rule is that `isSubmitting` and per-field errors are never `useState`. Two
things satisfy it, and which one is not a preference:

**React Hook Form + `zodResolver` is the default.** shadcn's `<Form>` *is* React
Hook Form, so any form using it is already RHF. See FORMS.md.

```tsx
const form = useForm({ resolver: zodResolver(Schema), defaultValues: { username: "" } });
// form.formState.isSubmitting — never a useState flag
```

**`useActionState` for a form with one submit, one error, and no per-field
feedback** — a search box, a confirm button. No dependency, uncontrolled inputs.

```tsx
const [error, submitAction, isPending] = useActionState(
  async (_prev: string | null, formData: FormData) => {
    const result = await createPost({ title: formData.get("title") as string });
    return result.error ?? null;
  },
  null,
);

return (
  <form action={submitAction}>
    <input name="title" required />
    <button type="submit" disabled={isPending}>{isPending ? "Posting…" : "Create"}</button>
    {error && <p className="text-destructive">{error}</p>}
  </form>
);
```

Never both on one form — `handleSubmit` and `<form action>` both own submission.

### 6. `useFormStatus` Reads Form State Without Prop Drilling

A reusable submit button reads its parent `<form>`'s pending state directly.

```tsx
import { useFormStatus } from "react-dom";

// GOOD: no isPending prop threaded down
function SubmitButton({ children }: { children: React.ReactNode }) {
  const { pending } = useFormStatus();
  return <button type="submit" disabled={pending}>{pending ? "Saving…" : children}</button>;
}
```

`useFormStatus` must be called from a component rendered *inside* the `<form>`.

### 7. `useOptimistic` for In-Flight UI

```tsx
const [optimisticMessages, addOptimistic] = useOptimistic(
  messages,
  (state: Message[], text: string) => [...state, { id: crypto.randomUUID(), text, sending: true }],
);
// call addOptimistic(text) inside the action; React reverts automatically on settle
```

### 8. Manual `useMemo` / `useCallback` / `React.memo` Is an Anti-Pattern

React Compiler v1.0 is stable and auto-memoizes. Hand-written memo is now boilerplate.

```tsx
// BAD: noise the compiler already handles
const UserCard = React.memo(({ user }: { user: User }) => {
  const name = useMemo(() => formatName(user.name), [user.name]);
  const onClick = useCallback(() => selectUser(user.id), [user.id]);
  return <button onClick={onClick}>{name}</button>;
});

// GOOD: write it plainly — the compiler memoizes
function UserCard({ user }: { user: User }) {
  return <button onClick={() => selectUser(user.id)}>{formatName(user.name)}</button>;
}
```

Three narrow exceptions remain (third-party reference equality, measured-expensive computation, no-compiler codebases). See PERFORMANCE.md.

### 9. Stable Keys — Never Index Keys for Dynamic Lists

Index keys corrupt state when the list reorders, inserts, or deletes.

```tsx
// BAD: index key
{todos.map((todo, i) => <TodoItem key={i} todo={todo} />)}

// GOOD: stable identity
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}

// GOOD: key to remount-and-reset a subtree on identity change
<ProfileForm key={userId} userId={userId} />
```

### 10. Don't Initialize State From Props Without Care

```tsx
// BAD: stale forever — prop changes never update the state
function Editor({ initialText }: { initialText: string }) {
  const [text, setText] = useState(initialText);
}
```

If the copy must reset when the prop changes, use a `key` (gotcha 9). If the parent owns the value, lift state up instead.

### 11. Rules of Hooks — Call Them at the Top Level

Hooks are tracked by call order. They must run at the top level of a component or custom hook — never inside conditions, loops, or after an early return.

```tsx
// BAD: hook after an early return — call order changes when `user` is null
function Profile({ user }: { user: User | null }) {
  if (!user) return null;
  const [tab, setTab] = useState("home"); // skipped on some renders
}

// GOOD: all hooks first, unconditionally; branch afterwards
function Profile({ user }: { user: User | null }) {
  const [tab, setTab] = useState("home");
  if (!user) return null;
}
```

`use()` is the *only* hook that may be called conditionally (gotcha 3). Every other hook — `useState`, `useEffect`, `useReducer`, custom hooks — is top-level only. See USE-EFFECT.md for the full Effect catalog.

## When to Use What

| Decision | Use |
|---|---|
| Single value, toggle, form field | `useState` |
| Multiple related values updated together | `useReducer` |
| Value computable from props/state | derive during render — not state |
| Read context | `use(Context)` — never `useContext` |
| Global, rarely-changing value (theme, auth, locale) | Context |
| Pass UI down 2-3 levels | `children` / slot props — not Context |
| Form submission | `<form action>` + `useActionState` |
| Submit button pending state | `useFormStatus` |
| Optimistic UI during an action | `useOptimistic` |
| Non-urgent state update | `startTransition` |
| Reset a subtree on identity change | `key` prop |
| Component, hook, or file too large | split — see COMPONENT-DESIGN.md |
| Data fetching | TanStack Query, or `use()` + Suspense — not a raw Effect |

## TypeScript Integration

```tsx
// Extend HTML element props
interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
  variant?: "primary" | "secondary" | "destructive";
  isLoading?: boolean;
}

// Generic components
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  getKey: (item: T) => string;
}
function List<T>({ items, renderItem, getKey }: ListProps<T>) {
  return <ul>{items.map(item => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}

// Event types
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {};

// Children: always React.ReactNode
interface CardProps { title: string; children: React.ReactNode; }

// Discriminated-union props — make impossible prop combinations unrepresentable
type AlertProps =
  | { variant: "inline"; onDismiss?: never }
  | { variant: "toast"; onDismiss: () => void };   // a toast REQUIRES onDismiss
```

For general type design — discriminated unions, generics, `satisfies`, type guards — see the `typescript-best-practices` skill. This section covers only React-specific typing; hook typing (`useState`/`useReducer`/`useRef`, custom-hook returns, nullable Context) is in COMPONENT-DESIGN.md.

## Error Boundaries

Granular, per-section — never one app-wide catch.

```tsx
import { ErrorBoundary } from "react-error-boundary";

function Dashboard() {
  return (
    <div>
      <ErrorBoundary FallbackComponent={ErrorFallback}><AnalyticsChart /></ErrorBoundary>
      <ErrorBoundary FallbackComponent={ErrorFallback}><RecentOrders /></ErrorBoundary>
    </div>
  );
}
```

## Project Structure

One folder per domain. Inside it, every file lives in the folder named for its kind. The only file at a feature's root is `index.ts`.

```
src/
  features/destinos/
    components/   DestinoCard.tsx  DestinoCard.test.tsx
    hooks/        useGlobe.ts  useGlobe.test.ts       # not query hooks
    api/          destinos.ts  destinos.test.ts       # fetch + parse, no React
    queries/      destinos.ts                         # queryOptions, useQuery, useMutation
    schemas/      destino.ts                          # zod schemas + their z.infer types
    types/        destino.ts                          # types with no schema
    utils/        monthFromUrl.ts  monthFromUrl.test.ts
    test/         fixtures.ts                         # fixtures shared by this feature's tests
    index.ts                                          # public API only
  shared/         same kind folders, for code 2+ features use
  routes/ or pages/                                   # wherever the router wants them; thin
```

```
BAD  features/search/useDismiss.ts            hook at the feature root
BAD  features/legal/components.test.ts        one test file for many components
BAD  src/components/DestinoTable.tsx          domain component outside its feature
GOOD features/search/hooks/useDismiss.ts
GOOD features/legal/components/LegalPage.test.tsx
```

A test sits next to the file it tests and carries its name; in a project that already keeps tests in `__tests__/`, follow it. A test of a whole flow goes in `src/test/`. Create a kind folder when its first file arrives, not before. Import another feature through its `index.ts` only. Colocate first; move to `shared/` when 2+ features need it.

## Rules

1. **Always** use function components — no class components.
2. **Always** treat `ref` as a normal prop; never write `forwardRef` in new code.
3. **Always** render `<Context value={…}>` directly — never `<Context.Provider>`.
4. **Always** use `use()` to read context — never `useContext`.
5. **Always** derive state during render when a value is computable; don't sync it in an Effect.
6. **Always** use `<form action>` + `useActionState` for submissions; `useFormStatus` for nested submit buttons.
7. **Never** add `useMemo` / `useCallback` / `React.memo` in new code — the React Compiler handles memoization. See PERFORMANCE.md.
8. **Never** use an array index as a key for a dynamic list; use a stable id.
9. **Never** initialize state from a prop expecting it to update — use a `key` reset or lift state up.
10. **Never** call hooks conditionally, in loops, or after an early return — top level only. `use()` is the sole exception.
11. **Prefer** `children` / slot props over Context to avoid prop drilling.
12. **Prefer** granular per-section error boundaries; never rely on data-fetching Effects.
13. **Always** handle the loading / error / empty trio in data-driven components — see LOADING-STATES.md.
14. **Always** put a feature file in its kind folder (`components/ hooks/ api/ queries/ schemas/ types/ utils/`); never at the feature root, never in a top-level `src/components/`.

## Reference Files

For deeper guidance, load the file matching what you're working on:

- **COMPONENT-DESIGN.md** — read before writing a new component or hook, and when splitting one or a file is getting large. The question is "how should this be structured?", not "is this file already too big?". Covers the single-responsibility hard limits (≤200 lines/file, ≤30 lines/function, ≤50-line JSX return, ≤5-6 props, ≤3 nesting levels), split vs don't-split signals, one-concern-per-hook plus hook naming, composition vs Context vs prop drilling, early returns, file organization, the complexity smell test, and the server/client component boundary note.
- **USE-EFFECT.md** — read when writing or reviewing a `useEffect`. Covers the full "You Might Not Need an Effect" catalog (transforming data, resetting state via `key`, event handling, chained Effects, notifying parents, passing data to a parent, app init, fetching), the decision checklist, when Effects *are* correct plus cleanup and race-condition (`ignore` / `AbortController`) patterns, dependency-array discipline, and `useSyncExternalStore`.
- **PERFORMANCE.md** — read when chasing re-renders or memoization. Covers React Compiler v1.0 (delete manual memo; the three narrow exceptions; ESLint enforcement), keys and `startTransition`, `useDeferredValue` with its React 19 initial-value arg, `<Activity mode>` for pre-render and keep-alive, and resource preloading (`preload` / `preinit` / `prefetchDNS` / `preconnect`).
- **LOADING-STATES.md** — read when a component fetches data or shows async UI. Covers the loading / error / empty trio every data component must handle, skeleton screens vs spinners (and avoiding layout shift), building a reusable token-styled `Skeleton` primitive, `<Suspense>` + error-boundary pairing and boundary placement, empty-state design, and accessibility (`aria-busy`, `aria-live` / `role="status"`, focus management).
- **ZUSTAND.md** — read when client/UI state outgrows `useState` and Context. Covers typed curried store creation, atomic selectors and `useShallow` to prevent re-renders, exporting custom hooks over the raw store, event-driven actions, `persist` / `immer` / `devtools` middleware and composition order, the slices pattern, transient updates, and the rule that server state belongs in a query library, not a store.
- **FORMS.md** — read when building or reviewing a form. Covers React Hook Form with `zodResolver` and shadcn's `<Form>`, why a transforming schema needs `useForm<z.input<S>, unknown, z.output<S>>` instead of `z.infer`, `defaultValues` and the controlled/uncontrolled flip, reading `formState` instead of hand-rolled flags, putting server rejections on the field with `setError`, the Zod 3 → 4 differences, and the narrow case where `useActionState` is the better call.
- **SHADCN.md** — read before writing any UI in a repo that has a `components/ui/` directory, including the first screen of a new app, and when adding or editing a shadcn component. The question is "does this repo already have a design system I must use?", not "am I editing a shadcn file?". Covers Base UI as the default since July 2026 and how to tell which primitive a file uses, migrating one component at a time, why you edit `components/ui/*` instead of wrapping it, `cva` variants with `VariantProps` rather than boolean props, `cn()` and why `tailwind-merge` matters for overrides, `data-slot` for reaching into a child, the `Field` family for form layout, and what belongs in `components/ui/` versus `components/`.
- **TAILWIND-TOKENS.md** — read before writing any `className`, including the first one in a new app — picking colors, spacing, radius, or typography. The question is "what tokens does this repo already define?", not "am I restyling something?". Covers Tailwind v4 CSS-first `@theme` config and namespaces, semantic color tokens (never raw `red-500`), `@theme inline`, the `--color-*: initial` namespace reset for hard palette lockdown, the semantic spacing scale, the ban on arbitrary values (`p-[13px]`, `text-[#3a3a3a]`) and the narrow exceptions, plus ESLint enforcement tooling.

