React Audit
Purpose
Deep review of React code across three tracks: architectural analysis inline, mechanical convention checks in a subagent, and — when the binary is reachable — react-doctor. Reports only: no edits, no installs, nothing written into the project. After it runs the working tree is byte-identical.
Linter-aware. Biome and ESLint already own hook rules, dependency arrays, self-closing elements, fragments, type imports, and unused variables. This skill skips all of it.
When to Use
- After building or reworking a React feature, before it is committed
- Reviewing a change that touches components, hooks, or state
- A component works but has become hard to reason about — unexplained re-renders, an effect chain nobody wants to touch, a file that keeps growing
- Invoked as a stack lens by another skill — see Lens Mode
Trigger phrases: "review react", "audit react", "check components", "react patterns", "effects review", "why does this re-render".
Commands
| Command | Scope |
|---|---|
/react-audit |
Staged and unstaged changes |
/react-audit path/file.tsx |
One file |
/react-audit src/components/ |
A directory |
Phase 1: Load Context
1.1 Confirm this is a React project
Check package.json for a react dependency. If there is none, stop and say so — do not audit a
non-React codebase against React rules.
1.2 Detect the React version and the compiler
Both gate which findings are even valid, so resolve them before analysis, not during it.
jq -r '.dependencies.react // .devDependencies.react // "none"' package.json
grep -rl "babel-plugin-react-compiler\|reactCompiler" --include="*.config.*" --include="package.json" . | head -3
- React 19+ —
useActionState,useTransitionfor pending state,refas a plain prop, ref callback cleanups, anduse()are all available.useEffectEventis stable from 19.2. - React 18 — none of the above. Do not recommend them.
- React Compiler enabled — skip every memoization finding. The compiler inserts memoization
itself, so
useMemo/useCallback/memoadvice is noise there.
Carry all three answers into Phase 2; the reference files describe how each one gates their rules.
1.3 Read project rules
Read these if they exist, skip silently if not:
.cursor/rules/react.mdc
.cursor/rules/typescript.mdc
.cursor/rules/stores.mdc
.claude/rules/react.md
.claude/rules/typescript.md
With no project rules, fall back to the defaults in references/.
1.4 Detect conventions
grep -rl "\.displayName\s*=" src/ --include="*.tsx" | head -3
grep -rl "data-component" src/ --include="*.tsx" | head -3
displayNamefound in 2+ files → activate thedisplayNameanddata-componentchecks- Not found → skip both entirely, so projects without the convention get no false positives
props-naming,variable-order,component-props-ref, anddestructuringare always active
1.5 Identify target files
$ARGUMENTSgiven → that file or directory- Otherwise →
git diff --name-only HEADplusgit diff --name-only --cached - Filter to
.tsxand.tsfiles that import React - List the surviving files, then open all of them in one response before writing any analysis. A file you reasoned about without opening is not audited, and must not appear in the report
If the filter leaves nothing, report that there are no React files in scope and stop.
Close Phase 1 with one line: Scope: 6 files · React 19.1 · compiler off · conventions: displayName, data-component.
Phase 2: Dispatch Tracks
Start all applicable tracks at once so they run concurrently.
Track A: react-doctor (optional)
An external binary, so treat it as a bonus rather than a dependency. Create a summary file and a
diagnostics directory with mktemp, then run the bundled script with the scope that matches
Phase 1.5:
bash <skill-dir>/scripts/run-react-doctor.sh . "$OUT" "$DIAG" --scope files --include-untracked
| Phase 1.5 resolved | Scope args |
|---|---|
| Working-tree changes | --scope files --include-untracked |
| A file or directory | none — react-doctor scans the project, so filter its findings to the target paths in Phase 3 |
| A base ref (lens mode) | --scope changed --base <ref> |
The script needs nothing installed. It prefers a react-doctor already in the project or on PATH,
then falls back to bunx, pnpm dlx, or npx to fetch it for the run. bunx is first on purpose:
it resolves from its own cache, where npx will populate the nearest node_modules when it finds a
package.json above the working directory — a side effect an audit should not have. It writes nothing into the
project and opts out of telemetry, the score API, and crash reporting, so no source metadata leaves
the machine — drop --no-score from the script if you want the 0-100 health score back and the
network call that computes it is acceptable.
Read $DIAG/diagnostics.json, not the terminal summary. It is an array of objects with
filePath, line, endLine, rule, severity, category, title, message, and help —
everything Phase 3 needs to merge and dedupe, with no text to parse:
jq -r '.[] | "\(.filePath):\(.line) [\(.category)/\(.severity)] \(.rule) — \(.title)"' "$DIAG/diagnostics.json"
The directory also holds one .txt per triggered rule, each with the rule's fix guidance and a docs
URL. Read those only for rules you are about to report.
Exit code says whether the tool ran, never whether the code is clean. A warning-level finding
still exits 0. Always read diagnostics.json.
| Exit | Meaning | Do |
|---|---|---|
| 3 | No react-doctor binary and no bunx/pnpm/npx to fetch one | Skip Track A, continue |
| 4 | Present but would not start — offline, or resolution failed | Skip Track A, continue |
| 0 or 1 | Ran | Read diagnostics.json |
A skipped Track A is normal, not an error. Never install anything to satisfy it, never retry, and never block the other tracks on it. The report says the track was skipped and why; Tracks B and C carry the audit on their own.
Run it in the background if the host supports that — the other tracks do not depend on it.
Track B: Mechanical checks (subagent)
- Read
references/mechanical-checks-prompt.mdfor the prompt template - Read
references/rules-conventions.mdfor the rules - Replace
{{CONVENTIONS}}with only the conventions Phase 1.4 activated, and{{FILE_LIST}}with the target paths - Dispatch a subagent with that prompt to scan the target files against the active convention rules and return structured violations, each with a file, a line, the rule it breaks, and a confidence rating. The subagent reports everything it finds; Phase 3 does the filtering
These are read-only pattern matches, so a cheap subagent is enough. If the host has no subagent facility, run the same prompt inline.
Track C: Deep analysis (inline)
Load references/rules-effects.md and references/rules-patterns.md, then analyze each target file:
Effects — the 16 anti-patterns in rules-effects.md. Flag each with its number and the fix.
Patterns 15 and 16 are React 19-only; #4 is void when the compiler is on.
Patterns — memoization strategy (skip entirely under the compiler), ref.current in dependency
arrays, ref callbacks and their cleanups, early returns versus conditional rendering, throttle and
debounce construction, context splitting, data fetching in effects over ~15 lines, components over
200 lines, and related useState calls that should be a useReducer.
Close Phase 2 with one line naming what each track returned:
Tracks: A 14 diagnostics · B 5 violations · C 9 findings. A skipped track appears with its reason.
Phase 3: Collect and Merge
- Read
$DIAG/diagnostics.jsonif Track A ran, whatever its exit code, and filter to the target paths. Treat each entry as a hypothesis, not a verdict: open the file atlineand confirm it before reporting, the same standard Track C is held to - Collect the Track B violations and resolve their confidence ratings. A
highone ships as found. For everymediumandlow, open the cited line and check it againstreferences/rules-conventions.md: confirmed ships, contradicted is dropped, and one the file cannot settle ships tagged(unconfirmed)after the rule name. Never report alowyou did not open - Combine with Track C
Deduplicate. Same file, same line range, same category from more than one track: keep the most
detailed version and record every track that found it. react-doctor and Track C overlap most — a
no-fetch-in-effect diagnostic and Effects #13 are usually one finding, and react-doctor's help
text plus the rule's .txt file often sharpen the fix Track C would have written alone.
Drop what is out of scope. Anything Biome or ESLint already reports, and anything outside the target paths.
Close Phase 3 with one line carrying both counts: 28 raw findings -> 17 after dedupe and scope filter.
Phase 4: Output
Buckets
| Bucket | Holds | Ordering |
|---|---|---|
| Critical | Causes a bug or wrong behavior — races, stale closures, effects that fire wrong | First |
| Improvements | Architecture and pattern changes that are judgment calls | Second |
| Conventions | Project convention violations from Track B | Last, always |
Conventions rank below everything else and never gate anything. They are cleanup-shaped, not
correctness-shaped: report them so the picture is complete, then hand them to review:code-cleanup,
which is the skill that actually applies changes.
Example output
One file section per file, then one summary table for the whole run:
## src/components/ItemList.tsx
### Critical
**1. Race condition in fetch effect** (`ItemList.tsx:45-58`) [Effects #13]
**Current:** Raw fetch in useEffect without cleanup — a stale response can overwrite a fresh one
**Fix:** Add an ignore flag in the cleanup, or move to the project's `useQuery` wrapper
**Found by:** Deep analysis, react-doctor
### Improvements
**2. Extract data fetching to a custom hook** (`ItemList.tsx:30-72`) [Patterns]
**Current:** 40 lines of fetch logic in the component body
**Fix:** `useItemData`, following the project's existing `use*Data` pattern
**Found by:** Deep analysis
### Conventions
**3. Missing displayName** (`ItemList.tsx`) [displayName]
**Fix:** `ItemList.displayName = ITEM_LIST_NAME;`
**Found by:** Mechanical check
## Summary
Tracks: deep analysis, mechanical checks. react-doctor skipped (exit 3 — no runner available).
React 19.1, compiler off.
| # | Finding | Bucket | Location | Found by |
|---|---------|--------|----------|----------|
| 1 | Race condition in fetch effect | Critical | `ItemList.tsx:45-58` | Deep, RD |
| 2 | Extract data fetching to a hook | Improvement | `ItemList.tsx:30-72` | Deep |
| 3 | Missing displayName | Convention | `ItemList.tsx` | Mech |
Open with the track line. Which tracks ran, which were skipped and why, the React version, and whether the compiler is on. A reader cannot judge coverage without it.
Show the before and after for anything non-obvious, and name the project's own pattern when one exists. Where the current code is a defensible trade-off rather than a mistake, say so in the finding instead of dropping it.
No findings
## React audit: clean
Tracks: deep analysis, mechanical checks, react-doctor. React 19.1, compiler off.
4 files in scope, no findings.
The setup recommendation, when it applies, is the only thing that may follow the summary table — see the next section. Then stop. Do not apply a finding, do not offer to apply one, and do not start a second pass.
Recommending a Permanent Setup
Track A always uses the ephemeral runner — fetched for the run, cached outside the project, gone afterwards. That holds even when you are about to recommend a permanent setup, and it holds if the user says yes: they run the install, not you.
A permanent setup is worth recommending anyway, because the ephemeral run is a snapshot. Installed,
react-doctor runs from a doctor script, scans each PR in CI, and is available to the agent between
audits.
Recommend it when all of these hold:
- The repo is large enough to keep paying off. 20+ React files, or an established component tree. A handful of components does not need a CI gate
- Nothing already sets it up. No
doctor.config.{ts,mts,cts,js,mjs,cjs,json,jsonc}, noreactDoctorkey inpackage.json, no react-doctor dependency, no react-doctor CI workflow - The scan actually found something. Recommend on any
error-severity entry indiagnostics.json, or on a real volume ofwarning-severity ones. A clean scan is an argument against the recommendation, not for it
jq -r 'group_by(.severity)[] | "\(.[0].severity): \(length)"' "$DIAG/diagnostics.json"
Put it at the very end — the last lines of the output, after the summary table and after every finding. It is a suggestion about tooling, and it must never sit between the reader and the findings they came for:
react-doctor found N issues here (E errors, W warnings) and is not set up in this repo.
bunx react-doctor@latest installadds it as a dev dependency, adoctorpackage script, a skill for your agents, and a CI workflow that scans each pull request.
Recommend it, never run it — including with --dry-run. Observed behavior of install in
0.9.12: it adds a doctor script and a react-doctor devDependency to package.json, writes a
lockfile, populates node_modules, adds .github/workflows/react-doctor.yml, and drops a skill
directory into the config directory of every agent it detects. --dry-run printed a "would install"
list and the same writes still landed, so treat it as an install, not a preview.
Say it once, and drop it if the user has declined before.
Lens Mode
review:changes-review invokes this skill as a stack lens when the diff contains React files. In
that mode:
- The caller supplies the scope. Use exactly the files it resolved — do not widen to the project
- Return findings in the shape the caller asked for, not the format above
- Conventions map to the caller's lowest severity, and never to anything higher
- Track A uses
--scope changed --base <ref>so it reports only what the change introduced - Skip the setup recommendation entirely — a review report is not the place for it
- Everything else — the phases, the reference rules, the version gates — is unchanged
Standalone invocation is unaffected and stays the primary path.