# React Audit

> Audit idiomatic React (hooks correctness, component design, state management, React 18/19 idioms). Static-only by design with optional implementation plan.

- **Type:** Skill
- **Install:** `agentstack add skill-bensheridanedwards-architectplaybook-react-audit`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [BenSheridanEdwards](https://agentstack.voostack.com/s/bensheridanedwards)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [BenSheridanEdwards](https://github.com/BenSheridanEdwards)
- **Source:** https://github.com/BenSheridanEdwards/ArchitectPlaybook/tree/main/react-audit

## Install

```sh
agentstack add skill-bensheridanedwards-architectplaybook-react-audit
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# /react-audit

Audit a TypeScript and React project against an opinionated baseline of **idiomatic React** organised in four layers — **hooks correctness**, **component design**, **state management**, **React 18/19 idioms** — preceded by a diagnostic snapshot. Then offer to generate an implementation plan for the gaps.

This skill is scoped to React-correctness independent of cost, accessibility, or architecture. Many React concerns live in adjacent audits; the boundary table below is the canonical map.

## How this differs from neighbouring audits

The architect-playbook deliberately keeps audit boundaries tight so a single concern lives in a single skill. Every audit in the playbook touches React in some way; this is where the lines fall:

| Concern | Owner |
| --- | --- |
| Hook correctness (Rules of Hooks, dependency arrays, custom hook design) | `/react-audit` |
| Component-shape patterns (function components, forwardRef, displayName, naming) | `/react-audit` |
| State strategy at the React-API level (when to lift, when to derive, form/server/URL/component split) | `/react-audit` |
| React 18+ primitives usage (`useTransition`, `useId`, Suspense, `'use client'`, server actions, `useOptimistic`) | `/react-audit` |
| Memoization for **runtime cost** (performance-driven `useMemo`/`React.memo`/virtualization) | `/performance-audit` |
| Memoization for **correctness** (referential stability that downstream code depends on) | `/react-audit` |
| God components, file size, fan-out as architectural invariants | `/architecture-audit` |
| Single state-management library (Redux vs Zustand vs Jotai chosen once) | `/architecture-audit` |
| jsx-a11y rules and accessibility patterns inside components | `/accessibility-audit` |
| React error boundaries and Suspense + boundary pairing | `/error-handling-audit` |
| ESLint plugin coverage (which React rules are even enabled) | `/linting-audit` |
| Bundle-level concerns: `React.lazy` configuration, dynamic-import wiring | `/bundle-build-audit` |

When a single fix passes multiple audits (for example, enabling `eslint-plugin-react-hooks` satisfies both `/linting-audit` and `/react-audit`; choosing a single global state library satisfies `/architecture-audit` and `/react-audit`), every relevant audit surfaces the same gap so the user sees it once and resolves it once.

## Posture: static-only, no opt-in modes

Hook correctness is best caught by a typed AST analyser, which is exactly what `eslint-plugin-react-hooks` already is — and `/linting-audit` already verifies that plugin is configured. Re-running the plugin from this skill would be redundant. The non-lintable parts of this audit (state-strategy patterns, idiom adoption, hook-design quality) are static AST and pattern detection.

If the user wants a fresh real-run capture of hook violations, `/linting-audit --with-run` is the right tool. This skill stays static.

## Usage

```
/react-audit                                      # default: concise Top 5 + full report saved + ask about plan
/react-audit --worktree                          # create an isolated Git worktree, then run the audit there
/react-audit --learn                              # mid-level engineer teaching mode (detailed explanations + file/line examples)
/react-audit --teach                              # alias for --learn
```

**💡 Pro tip**: Add `--worktree` to run this audit in an isolated Git worktree.

The skill never accepts `--apply`. The implementation plan is descriptive Markdown.

This audit deliberately has no numeric threshold flags. Most checks are zero-tolerance or qualitative; class-component presence is a `partial` regardless of count. The canonical path to evolving the baseline itself is `/system-self-improve`.

## The opinionated baseline

A check resolves to one of four statuses:

- **present** — the invariant holds.
- **partial** — most signals resolve, with a small number of exceptions, or the codebase shows mixed adherence to a soft check.
- **missing** — a structural prerequisite is absent (no React installed, for example — the skill stops before this state is ever reached, but the status is reserved for future checks).
- **violation** — the audit identified concrete code that breaks the invariant.

Layer 0 is informational only and has no status. Layer 4 reports `skipped: "react-version-below-18"` (and no per-check entries) when React is older than 18.

### Layer 0 — Diagnostic snapshot (always written, no pass/fail)

- React version, resolved from `package.json` and the lockfile (the lockfile is authoritative when it disagrees with `package.json`).
- Framework variant (Next.js App Router or Pages Router, Remix, Vite-React, Create React App, plain React).
- Component counts: function components, class components, total. Class components are explicitly counted because their presence shapes layer 2.
- Custom hook count, plus the largest custom hook by line count (helps identify hooks that should be split).
- Detected form library: react-hook-form, formik, native, or none.
- Detected animation library: framer-motion, react-spring, native CSS animation/transitions, or none.
- Detected design system / component library: Radix UI, shadcn/ui, MUI, Chakra UI, Mantine, Ariakit, custom in-house, or none.
- Counts of new-React primitives in source: `useTransition`, `useDeferredValue`, `useId`, `useOptimistic`, `use()`, `useFormStatus`.
- `'use client'` and `'use server'` directive counts (App Router projects only; recorded as `null` otherwise).
- `forwardRef` usage count and `React.memo` usage count.

### Layer 1 — Hooks correctness

| Check | Expectation | Violation signal |
| --- | --- | --- |
| `eslint-plugin-react-hooks` recommended set enabled | The hooks plugin's recommended rules are enabled in the active ESLint configuration (or the equivalent Biome rules for projects on Biome). Overlap with `/linting-audit`; both surface. | Plugin missing, or recommended set off. |
| No silenced exhaustive-deps without justification | `// eslint-disable-next-line react-hooks/exhaustive-deps` lines have a justification comment on the same or adjacent line. Soft check — reported as `partial`. | Suppressions of `exhaustive-deps` with no surrounding explanation. |
| No empty dependency arrays where dependencies exist | `useEffect(() => { ... }, [])` is not used when the body legitimately closes over rendered values that change. | Effects with empty dependency arrays that read state, props, or context that varies across renders. |
| No data fetching in effects when a query library exists | When TanStack Query, SWR, RTK Query, Apollo Client, or framework-native loaders are available, components fetch via the data layer rather than `useEffect` plus `fetch`. (Overlap with `/performance-audit`'s "server state in data layer" check.) | `useEffect` calling `fetch`/`axios` in a project with a query layer installed. |
| No state synchronisation | State is derived from props or other state, not stored in `useState` and re-synchronised by `useEffect`. | Patterns where a `useEffect` does `setX(propX)` to mirror props into state. |
| Status enums over multiple booleans | When a component tracks multi-stage state, a single status enum is used (`'idle' | 'loading' | 'success' | 'error'`) rather than separate `isLoading`, `isError`, `isSuccess` booleans that can drift. Soft check — reported as `partial`. | Three or more boolean state variables that represent a single conceptual lifecycle. |
| `useRef` for mutable non-render state | Mutable values that don't trigger re-renders use `useRef`, not `useState`. | `useState` used to hold a value that is never read in render but mutated frequently. |
| Cleanup functions present for side effects | `useEffect` returns a cleanup for subscriptions, timers, observers, async-cancellation, and event listeners. | Side-effect setup with no matching teardown. |
| Memoization for correctness, not just performance | When a callback or value appears in another hook's dependency array and that downstream hook depends on referential stability for correctness (typically a `useEffect` dependency list), the callback or value is wrapped in `useCallback`/`useMemo`. Distinct from `/performance-audit`'s performance-driven memoization: this catches infinite-re-run bugs, not slow renders. | A `useEffect` whose dependencies include an inline callback created on each render. |
| Custom hook design | Custom hooks have a single responsibility, are named `useFoo`, and return a consistent shape across the codebase (single value for one return, tuple for two, object for three or more). Soft check — reported as `partial` when adherence is mixed. | Custom hooks returning mixed shapes inconsistently, or named without the `use` prefix. |

### Layer 2 — Component design

| Check | Expectation | Violation signal |
| --- | --- | --- |
| Function components only in new code | New components are function components; class components persist only as legacy. The audit reports class components as `partial` rather than `violation` because legacy migrations are slow. The strongest signal is a class component whose file has been modified more recently than the most recent function-component file — recorded explicitly. | Class components present (count recorded); class component newer than the most recent function component (flagged). |
| Components named in PascalCase | Component files and exported component identifiers use PascalCase. | Non-PascalCase component definitions. |
| Hooks named with `use` prefix | Custom hooks export identifiers starting with `use` followed by an uppercase letter. | A function that calls hooks but does not start with `use`. |
| `forwardRef` for ref-receiving components | Components that need to receive `ref` (typically design-system primitives, focusable controls) use `forwardRef`. React 19's `ref` as a prop is also accepted. | A component renders a focusable element that consumers would expect to ref, but does not forward refs and is not React-19 ref-as-prop. |
| `displayName` on `memo`/`forwardRef` wrappers | Components wrapped in `React.memo` or `forwardRef` set `displayName` so the React DevTools tree is readable. | Wrapped components with no `displayName`. |
| Children prop typed | When a component accepts children, the prop is typed `React.ReactNode` (not `any`, not `JSX.Element`, which is too narrow). | `children: any` or `children: JSX.Element` in component prop types. |
| One component or hook per file | Files export one primary component or hook plus its supporting types and helpers, not a grab-bag of unrelated components. Soft check — reported as `partial`. | Files exporting more than three independent components or hooks. |
| Named exports for components | Components are exported as named exports, not default exports. Improves grep-ability and tooling. Soft check — reported as `partial`. | Default-exported components in projects that otherwise use named exports. |

### Layer 3 — State management

| Check | Expectation | Violation signal |
| --- | --- | --- |
| Form state via a form library | Forms with more than five fields, or with non-trivial validation, use a dedicated form library (react-hook-form, formik). Native form state is acceptable for trivial forms. Soft check — reported as `partial`. | Complex forms hand-rolled with a `useState` per field. |
| Server state in the data layer | Server-fetched data lives in TanStack Query, SWR, RTK Query, Apollo Client, or framework-native loaders — not in component-local `useState` populated by `useEffect`+`fetch`. (Overlap with `/performance-audit` and `/architecture-audit`; all three surface.) | Fetched-data state held in component state with no query layer involved. |
| Single global state library | Exactly one of Redux Toolkit, Zustand, Jotai, MobX, or React Context as the global state strategy. (Overlap with `/architecture-audit`; both surface so a single fix passes both.) | Multiple global state libraries in the same application. |
| No state synchronisation between sources of truth | Data is stored in one place and derived elsewhere; the same value isn't held in component state and a global store and a URL parameter at once. | Detected duplicate-state patterns. |
| State at the right level | Local state lives in the component that uses it; lifted state lives at the lowest common ancestor; global state is for genuinely global concerns (auth, theme, feature flags) — not "anything used in two places". Soft check — reported as `partial`. | Global state holding values used by a single subtree, or local state for values used across the application. |
| URL state for shareable or recoverable state | Search queries, filters, sort orders, current page, and selected tabs live in URL parameters when the user would expect them to survive a reload. Soft check — reported as `partial`. | Detected patterns where a refresh would lose UI state that the user expects to keep. |

### Layer 4 — React 18/19 idioms (skipped silently when React below 18)

| Check | Expectation | Violation signal |
| --- | --- | --- |
| `useId` for accessible IDs | Generated IDs used to wire up labels, ARIA attributes, and form controls use `useId` rather than ad-hoc counters or random strings. (Overlap with `/accessibility-audit`.) | Hand-rolled ID generation when React 18+ is available. |
| `useTransition` or `startTransition` for non-urgent updates | Updates that can be deferred (filtering large lists, search-as-you-type, navigation in a Suspense-aware app) use `useTransition` or `startTransition`. Soft check — reported as `partial`. (Overlap with `/performance-audit`.) | Detected hot patterns where transitions would help and are absent. |
| Suspense boundaries for async data | When the framework or library supports Suspense for data fetching (React 19 `use()`, TanStack Query Suspense mode, framework loaders), Suspense boundaries with meaningful fallbacks are used. | Async data fetched without surrounding Suspense in projects that have opted into Suspense-based data fetching. |
| `'use client'` used minimally and deliberately (Next.js App Router only) | Client-component boundaries sit at the leaves of the tree, not at the root. (Overlap with `/architecture-audit`.) Soft check — reported as `partial` when more than half of components are client components. | Excessive `'use client'` fragmentation; or `'use client'` files importing server-only modules. Skipped silently outside App Router. |
| Server Actions used appropriately (Next.js App Router only) | Mutating operations in App Router projects use Server Actions (`'use server'`) when appropriate, rather than client-side `fetch` to a route handler that simply mutates. Soft check — reported as `partial`. | Patterns where Server Actions would be the natural choice but route handlers are used instead. Skipped silently outside App Router. |
| `useOptimistic` for optimistic mutations (React 19 only) | Optimistic UI updates use `useOptimistic` rather than hand-rolled optimistic state. Skipped silently for React 18.x. | Hand-rolled optimistic updates in React 19 codebases. |

## What this skill does

1. **Reads the knowledge graph when present.** Soft dependency: when `graphify-out/graph.json` exists, the audit identifies widely-consumed custom hooks (a hook used by 50 components is more important to get right than one used by 2) and prioritises them in the implementation plan. The audit still runs in full when the graph is absent.
2. **Confirms a TypeScript and React project.** Detects `package.json`, `tsconfig.json`, and `react` in dependencies. If any are absent, the skill stops and tells the user it currently supports TypeScript and React projects only.
3. **Resolves the React version** from `package.json` and the lockfile. Determines whether layer 4 runs (React 18 or newer) or is reported as skipped.
4. **Detects framework, form library, animation library, and design system** for the diagnostic snapshot and for layer-specific dispatch (App Router gates tw

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [BenSheridanEdwards](https://github.com/BenSheridanEdwards)
- **Source:** [BenSheridanEdwards/ArchitectPlaybook](https://github.com/BenSheridanEdwards/ArchitectPlaybook)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-bensheridanedwards-architectplaybook-react-audit
- Seller: https://agentstack.voostack.com/s/bensheridanedwards
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
