~/skills/react-gauntlet

/react-gauntlet

React

A Claude Code skill that forces AI agents to justify every effect and validate component resilience before any React code ships.

React 19Claude Code

The problem

The most persistent class of React bugs has a common fingerprint: useEffect used where it doesn’t belong. State that could be derived at render time gets written to a variable and synchronized via an effect. User interactions get routed through effects to share logic. Components work in a sandbox but fail in SSR, portals, or concurrent mode — because the component was never designed to handle those environments.

Dan Abramov’s A Complete Guide to useEffect frames the core problem: effects are not lifecycle hooks. They describe a synchronization relationship between React and an external system. Every effect should have an external system on the other side — if it doesn’t, the effect doesn’t belong there. The React team’s Thinking in React extends this further: before asking how to synchronize state, ask whether the state should exist at all. Most derived values aren’t state — they’re calculations that belong at render time.

AI coding agents make all of this worse. Given a task that requires reactive behavior, the default move is an effect — not because the model doesn’t understand React, but because nothing in the prompt forced it to ask the structural questions first. The result is code that compiles, passes basic tests, and fails in ways that are expensive to trace.

react-gauntlet imposes those questions. It runs a five-gate review before and after any React implementation: preflight static analysis, effect justification, anti-pattern detection, resilience hardening, and a postflight static analysis pass. The model works through each gate explicitly and reports its verdicts — which forces the structural thinking that would otherwise get skipped in the interest of producing output faster.

How the gates work

The skill is designed for React 19+ with the React Compiler enabled. That assumption changes several things: manual useMemo and useCallback for performance are treated as anti-patterns the compiler already handles, and React 19 primitives like use(), useOptimistic, and Server Actions eliminate entire categories of effects before Gate 1 even runs.

Gates 0 and 4 are the static analysis bookends — preflight before any code is written, postflight after all changes are complete. eslint-plugin-react-hooks and TypeScript are run at both points because the refactoring work in Gates 1–3 routinely introduces new violations: restructured effects, lifted state, and stabilized references all touch dependency arrays in ways that create fresh lint errors. A clean Gate 0 does not stay clean through implementation. The exhaustive-deps rule is specifically required — it mechanically catches the dependency gaps that cause stale closure bugs. Suppressing it without a logged justification is banned.

Gate 1 asks whether each effect is necessary at all. Before the six-question test, it prompts the model to consider React 19 alternatives — use() for data fetching, useOptimistic for transient state, Server Actions for mutations. If none apply, it runs the standard justification questions: is there an external system? Could this be rendered? Could it be event-driven? Is the state minimal and correctly owned? Are the dependency declarations honest?

Gate 2 is a lookup table of anti-patterns updated for the React 19 + compiler baseline. Manual performance memoization is listed as an anti-pattern outright — not conditionally. Two new rows cover use() and useOptimistic as preferred alternatives for patterns that previously required effects.

Gate 3 works through ten resilience dimensions drawn from Shu Ding’s catalog of real-world component failures. The tenth dimension — previously “future-proof” — is now “compiler-proof”: it explicitly names the React Compiler as active and flags manual performance memos as violations, not just risks.

the skill

Save as ~/.claude/skills/react-gauntlet.md and invoke with /react-gauntlet at the start of any relevant task.

react-gauntlet.md
---
name: react-gauntlet
description: Mandatory pre/post implementation review for React 19+ code with the React Compiler active. Forces effect justification, static analysis, and resilience hardening before any component work is finalized.
---

# react-gauntlet

A structured gauntlet for React code — run before implementation and again before finalizing. Assumes **React 19+ with the React Compiler enabled**. Every effect must justify its existence. Every component must survive ten resilience scenarios.

## Sources of truth

- https://react.dev/learn/you-might-not-need-an-effect
- https://shud.in/thoughts/build-bulletproof-react-components
- https://overreacted.io/a-complete-guide-to-useeffect/ (dependency model, stale closures)
- https://react.dev/learn/thinking-in-react (minimal state, ownership)
- https://kentcdodds.com/blog/usememo-and-usecallback (memoization heuristics)
- https://react.dev/learn/react-compiler (compiler handles performance memoization)

If sources conflict, prefer official React docs for baseline correctness and treat the bulletproof article as hardening guidance.

## When to use

Use this skill whenever the task involves:

- Creating or refactoring React components
- `useEffect`, `useSyncExternalStore`, or any hook involving side effects
- State synchronization, derived state, or data fetching
- Providers, context, SSR/hydration, portals, or view transitions
- Parent/child data flow or event-handling logic

## Non-negotiable process

Execute in order. Do not skip steps. Do not output code only.

1. **Gate 0 — Preflight static analysis:** Resolve all ESLint and TypeScript errors before writing code.
2. **Gate 1 — Effect necessity:** Audit every `useEffect`. Justify or eliminate.
3. **Gate 2 — Anti-pattern scan:** Check against known React 19 pitfalls.
4. **Gate 3 — Resilience scan:** Validate the component against runtime environment risks.
5. **Gate 4 — Postflight static analysis:** Re-run ESLint and TypeScript after all changes. Implementation work in gates 1–3 routinely introduces new violations.
6. **Final report:** Output checklist verdicts and residual risks.

---

## Gate 0: Preflight static analysis

Run before implementation begins.

**ESLint (`eslint-plugin-react-hooks`):**
- `react-hooks/rules-of-hooks` — hooks called unconditionally, in the correct call order
- `react-hooks/exhaustive-deps` — dependency arrays are complete; no missing or suppressed dependencies

**TypeScript strict mode:**
- Props and state have explicit, narrow types; no implicit `any`
- Callback signatures match their consumers
- Server/client boundaries correctly typed

Resolve all errors and warnings before proceeding. Do not suppress rules (`// eslint-disable`) without logging the justification in the final report.

---

## Gate 1: Effect necessity

An effect describes a synchronization relationship between React and an external system. If you cannot name the external system, the effect almost certainly does not belong.

**Before reaching for `useEffect`, consider React 19 alternatives:**
- Data fetching: `use(promise)` with a Suspense boundary reads a promise in render without an effect.
- Optimistic state: `useOptimistic` provides transient optimistic values without effect-based synchronization.
- Mutations and revalidation: Server Actions with `useActionState` handle submissions and side effects server-side.

For every remaining or proposed `useEffect`, answer:

1. **What external system is being synchronized?**
   - Allowed: browser APIs, non-React DOM widgets, subscriptions, timers tied to component lifecycle.
   - If no external system exists, the effect is almost certainly unnecessary.

2. **Could this be computed during render instead?**
   - Data derived from props or state belongs at render time, not in `useEffect` + `setState`.
   - The React Compiler handles expensive computations automatically — do not add `useMemo` for performance.

3. **Could this be triggered from an event handler instead?**
   - User-initiated work — mutations, toasts, navigation — belongs in event handlers where causation is explicit.

4. **Is the state minimal and correctly owned?**
   - Every value that can be derived from existing props or state should be removed from state and computed at render time.
   - If multiple components need the same state, it belongs at their lowest common ancestor — not synchronized via effects.
   - Reset a component subtree with `key` rather than syncing stale state via effects.

5. **If the effect must remain, is cleanup correct?**
   - Subscriptions and listeners are removed on unmount.
   - Data fetches guard against race conditions with an `ignore` flag or `AbortController`.

6. **Are dependency declarations honest?**
   - Every value read inside the effect must appear in the dependency array. Suppressing `exhaustive-deps` to reduce re-runs is a bug, not a fix — the effect will silently capture stale values.
   - To read previous state without subscribing to it, use the functional update form (`setState(prev => ...)`) or `useReducer`.
   - To stabilize a function or object that recreates on every render, restructure to eliminate the effect, or use `useCallback` at the call site only if the compiler cannot infer stability.

Refactor any effect that fails this gate before proceeding.

---

## Gate 2: Anti-pattern scan

| Anti-pattern | Preferred approach |
|---|---|
| `useEffect` + `setState` to derive from props/state | Compute during render |
| Effect chains (effects that trigger other effects) | Batch all updates in a single event handler |
| `useEffect` to notify parent of state change | Update parent and local state together in the event handler |
| `useEffect` to reset state when a prop changes | Use `key` or calculate directly during render |
| Manual `addEventListener` for an external store | `useSyncExternalStore` |
| `useMemo` or `useCallback` added for performance | Remove: the React Compiler handles this automatically |
| `useMemo` relied on for semantic correctness | Use `useState` for values that must persist |
| `useEffect` for one-time app initialization | Module-level code or a `didInit` guard flag |
| Redundant state that mirrors a prop | Remove the state; read the prop directly |
| Fetch inside `useEffect` without stale-response protection | Add an `ignore` flag or `AbortController` to the cleanup |
| Suppressing `exhaustive-deps` to avoid re-runs | Restructure: use functional setState, `useReducer`, or remove the effect |
| `useEffect` for data fetching with Suspense available | Use `use(promise)` in render with a Suspense boundary |
| `useState` + `useEffect` for optimistic UI | `useOptimistic` |

---

## Gate 3: Resilience scan

Review against each dimension; mark `n/a` for clearly inapplicable ones.

1. **Server-proof** — No `window`, `document`, or `localStorage` at render time. Browser APIs moved to `useEffect` or guarded with `typeof window !== 'undefined'` only where necessary.

2. **Hydration-proof** — Client-only state (theme, auth, persisted preferences) avoids hydration mismatch. Use inline scripts or `suppressHydrationWarning` only with documented rationale.

3. **Instance-proof** — No hardcoded global IDs. Use `useId()` for stable unique identifiers across multiple component instances.

4. **Concurrent-proof** — Server data fetches deduplicated within a single request using `React.cache()` or framework-level caching (e.g., Next.js `fetch` deduplication).

5. **Composition-proof** — Data passed through component trees uses Context or explicit props rather than `React.cloneElement`, so Server Components and async children compose correctly.

6. **Portal-proof** — Event listeners bind to `ownerDocument.defaultView` rather than `window` to work correctly in portals, iframes, and popout windows.

7. **Transition-proof** — State updates that should animate across view transitions are wrapped with `startTransition`.

8. **Activity-proof** — Components that inject global styles or side effects clean up explicitly on unmount so hidden components don't pollute the DOM.

9. **Leak-proof** — Sensitive server values never cross the server/client boundary. Use `taintUniqueValue` / `taintObjectReference` in App Router where applicable.

10. **Compiler-proof** — The React Compiler is active. Do not rely on `useMemo` or `useCallback` for performance — the compiler manages this. Only use `useMemo` when referential stability is a correctness requirement that the compiler cannot guarantee. Values requiring semantic persistence live in `useState`.

---

## Gate 4: Postflight static analysis

Re-run the same checks from Gate 0 after all implementation changes are complete.

Restructuring effects, lifting state, and stabilizing references in Gates 1–3 frequently introduces new dependency array violations or type errors. A clean Gate 0 does not stay clean through implementation.

- Re-run `react-hooks/exhaustive-deps` and `react-hooks/rules-of-hooks`
- Re-run TypeScript compiler in strict mode
- Fix any new violations before reporting. Do not suppress.

---

## Required output format

**Before coding:**

```
Effect Inventory:
- [effect description]: keep | refactor | remove — [one-line reason]

Risk Matrix:
- Gate 0: pass | pending
- [Gate 3 dimension]: pass | needs change | n/a
```

**After coding:**

```
Changes made:
- [gate violation] → [fix applied]

Final Checklist Verdict:
- Gate 0 (Preflight static analysis):  pass | fail
- Gate 1 (Effect necessity):           pass | fail
- Gate 2 (Anti-pattern scan):          pass | fail
- Gate 3 (Resilience scan):            pass | fail
- Gate 4 (Postflight static analysis): pass | fail

Residual risks: [list or "none"]
```

---

## Decision defaults

When ambiguous, default to:

1. Fewer effects
2. Simpler state model
3. Event-driven updates over effect-driven updates
4. Render-time derivation over synchronized duplicate state
5. Explicit cleanup for any external synchronization
6. No manual memos for performance — the React Compiler handles this
7. State ownership: when multiple components need the same state, lift it to their lowest common ancestor rather than copying and synchronizing via effects

## Failure policy

If a requested approach violates a gate:

1. Name the gate and the specific violation.
2. Propose the nearest compliant alternative.
3. Implement the compliant path unless the user explicitly overrides — and log the deviation in the final report.

Essential reading

These articles are the intellectual foundation of each gate. Reading them gives you the judgment to apply the checklist — and to recognize when the model gets it wrong.