React (web)

React web applications.

What this is

Web React. Nearly half its rules are about hooks discipline — derived state synced via useEffect instead of computed during render, missing effect cleanup, conditional hook calls, exhaustive-deps disabled — because that's where render-cycle bugs hide. The rest cluster around state correctness (direct mutation, index keys on reorderable lists, server data copied into local state) and render performance (barrel imports, statically-imported heavy components, sequential awaits that should be parallel).

How to onboard it

A repository picks up this stack by onboarding on a profile that includes it. redline init detects the profile from what is in the repository, so in most cases this is automatic:

terminal
$ npx redlinegate init  # detects the profile
$ npx redlinegate init --profile fullstack-node  # or name one

4 profiles pull these rules in: fullstack-node, mobile-rn, web-next, web-react.

Once onboarded, your files match this stack when they fit any of these globs:

  • **/*.tsx
  • **/*.jsx
  • src/**/*.ts
  • app/**/*.ts
  • packages/**/src/**/*.ts
  • apps/**/src/**/*.ts

How to use it — 24 rules

Nothing to run. Once your profile includes React (web), redline init renders these rules into your repository's AI tooling and the reviewer applies them on every pull request. When a review comment cites one of these ids, this table is where to look up what it catches and why.

Rule idSeverityCatches
react/effect-derived-stateBLOCKER`useEffect` for derived state. State computable from existing props/state must be computed during render, never synced via effect.
react/effect-for-event-logicBLOCKER`useEffect` for event logic. Notifications, analytics, navigation triggered by a user action belong in the event handler, not an effect watching state.
react/missing-effect-cleanupBLOCKERMissing `useEffect` cleanup for subscriptions, timers, listeners, and in-flight requests (AbortController).
react/direct-state-mutationBLOCKERDirect state mutation (`arr.push`, `obj.x =`, `.splice`) followed by `setState` — silent update failure.
react/conditional-hook-callsBLOCKERConditional hook calls — hooks inside `if`, loops, or early returns.
react/key-is-indexBLOCKER`key={index}` on lists that reorder, insert, or delete.
react/server-data-in-local-stateBLOCKERServer data copied into local state. With TanStack Query / SWR, the query result IS the source of truth — never `useEffect(() => setLocal(data))`.
react/nested-component-definitionBLOCKERComponent defined inside another component — remounts every render, loses state.
react/useformstatus-placementBLOCKER`useFormStatus` in the same component that renders the `<form>` — always returns `pending: false`; must be in a child.
react/promise-in-renderBLOCKERPromise created during render passed to `use()` — infinite loop; promise must come from props, state, or cache.
react/exhaustive-deps-disabledBLOCKER`eslint-disable react-hooks/exhaustive-deps` — hides stale-closure bugs; require refactor instead.
react/incomplete-dependency-arrayHIGHIncomplete dependency arrays (stale closures).
react/sequential-awaitsHIGHSequential `await`s for independent operations — require `Promise.all`.
react/barrel-file-importsHIGHBarrel-file imports (`import { X } from '@/components'`) — require direct imports; barrels bloat bundles and cause circular deps.
react/heavy-component-static-importHIGHHeavy components (charts, editors, modals) imported statically — require `next/dynamic` / `React.lazy`.
react/uncontrolled-to-controlledHIGH`useState(undefined)` for controlled inputs — uncontrolled→controlled warning; use `''`.
react/missing-error-boundaryHIGHMissing Error Boundary around subtrees that fetch or can throw.
react/falsy-and-renderingHIGH`&&` conditional rendering with a possibly-falsy non-boolean left side (`count && <X/>` renders `0`) — require ternary or explicit boolean.
react/trivial-memoisationSUGGESTION`useMemo`/`useCallback` wrapping trivial primitives — remove.
react/missing-lazy-initialiserSUGGESTIONExpensive `useState` initializer without lazy init — pass a function.
react/starttransition-for-non-urgentSUGGESTION`startTransition` for non-urgent updates driving expensive renders.
react/oversized-componentSUGGESTIONComponent over ~300 lines — suggest split.
react/prop-drillingSUGGESTIONProp drilling beyond 2–3 levels — suggest composition or context.
react/prefer-ref-over-stateSUGGESTIONInteraction state read only inside callbacks — use a ref, not state, to avoid re-renders.

Expected output

A finding from this file, and every finding Redline produces, opens with a machine-readable first line — severity, then the rule id in brackets, then the problem in one line:

a finding from this file
Redline/BLOCKER [react/effect-derived-state]: <one-line problem>

Ids are aggregated per rule, which is how the organisation finds out which rules earn their place and which only generate noise — so a finding without a valid id cannot be measured and counts as untagged. Of the 24 rules here, 11 BLOCKER, 7 HIGH and 6 SUGGESTION. Only a BLOCKER must not merge; a SUGGESTION may be dismissed without justification, and is never upgraded to get attention.

If a rule here fires constantly on code your team has deliberately decided to allow, that is the signal to raise with the standards owner — the rule id is what makes that conversation measurable — not to argue it away comment by comment.

How to edit it

Rules are edited in standards/stacks/react.md and nowhere else. The rendered copies in AGENTS.md, .github/copilot-instructions.md and .github/instructions/ are generated and are overwritten by the next render. A change here propagates to every onboarded repository as a pull request, so treat it as a production change.

  1. Edit the markdownstandards/ is the only place a human edits a rule. Everything under AGENTS.md, .github/copilot-instructions.md and .github/instructions/ is rendered from it and is overwritten by the next render.
  2. node scripts/assign-rule-ids.mjsAssigns a permanent <stack>/<slug> id to any new rule bullet and rewrites the file in place. Do not invent an id by hand. CI runs the same script with --check and fails if a rule is missing one.
  3. node scripts/render-self.mjsRe-renders this repository's own artifacts from the edited source. CI runs it with --check, so stale checked-in output fails the build.
  4. Bump standards/manifest.json → versionRequired in the same pull request as the rule change. Sync pull requests quote the version, so a repository's rendered artifacts always name where they came from.
  5. Add a CHANGELOG.md entryAlso in the same pull request. A standards change with no measurement is an opinion — record the seed score alongside it.
  6. node scripts/validate.mjsThe bundle self-check CI runs: manifest integrity, well-formed rule ids, the severity output contract surviving your edit, glob portability.

Rule ids are permanent. Rewording a rule is fine and keeps its id; renaming or removing an id orphans every historical telemetry record that cited it.

The full file

standards/stacks/react.md · 47 lines · 3.6 KB
# React Review Rules

**Scope:** web React. Where a file is also matched by the React Native rules, the
React Native rules extend these — they do not replace them. Do not flag web-only
concerns (DOM, `next/dynamic`, bundle splitting) on React Native files.

## BLOCKER — request changes

- `react/effect-derived-state` — **`useEffect` for derived state.** State computable from existing props/state must be computed during render, never synced via effect.

  ```tsx
  // WRONG
  useEffect(() => { setFullName(first + ' ' + last); }, [first, last]);
  // RIGHT
  const fullName = first + ' ' + last;
  ```

- `react/effect-for-event-logic` — **`useEffect` for event logic.** Notifications, analytics, navigation triggered by a user action belong in the event handler, not an effect watching state.
- `react/missing-effect-cleanup` — **Missing `useEffect` cleanup** for subscriptions, timers, listeners, and in-flight requests (AbortController).
- `react/direct-state-mutation` — **Direct state mutation** (`arr.push`, `obj.x =`, `.splice`) followed by `setState` — silent update failure.
- `react/conditional-hook-calls` — **Conditional hook calls** — hooks inside `if`, loops, or early returns.
- `react/key-is-index` — **`key={index}`** on lists that reorder, insert, or delete.
- `react/server-data-in-local-state` — **Server data copied into local state.** With TanStack Query / SWR, the query result IS the source of truth — never `useEffect(() => setLocal(data))`.
- `react/nested-component-definition` — **Component defined inside another component** — remounts every render, loses state.
- `react/useformstatus-placement` — **`useFormStatus` in the same component that renders the `<form>`** — always returns `pending: false`; must be in a child.
- `react/promise-in-render` — **Promise created during render passed to `use()`** — infinite loop; promise must come from props, state, or cache.
- `react/exhaustive-deps-disabled` — **`eslint-disable react-hooks/exhaustive-deps`** — hides stale-closure bugs; require refactor instead.

## HIGH

- `react/incomplete-dependency-array` — Incomplete dependency arrays (stale closures).
- `react/sequential-awaits` — Sequential `await`s for independent operations — require `Promise.all`.
- `react/barrel-file-imports` — Barrel-file imports (`import { X } from '@/components'`) — require direct imports; barrels bloat bundles and cause circular deps.
- `react/heavy-component-static-import` — Heavy components (charts, editors, modals) imported statically — require `next/dynamic` / `React.lazy`.
- `react/uncontrolled-to-controlled` — `useState(undefined)` for controlled inputs — uncontrolled→controlled warning; use `''`.
- `react/missing-error-boundary` — Missing Error Boundary around subtrees that fetch or can throw.
- `react/falsy-and-rendering` — `&&` conditional rendering with a possibly-falsy non-boolean left side (`count && <X/>` renders `0`) — require ternary or explicit boolean.

## SUGGESTION

- `react/trivial-memoisation` — `useMemo`/`useCallback` wrapping trivial primitives — remove.
- `react/missing-lazy-initialiser` — Expensive `useState` initializer without lazy init — pass a function.
- `react/starttransition-for-non-urgent` — `startTransition` for non-urgent updates driving expensive renders.
- `react/oversized-component` — Component over ~300 lines — suggest split.
- `react/prop-drilling` — Prop drilling beyond 2–3 levels — suggest composition or context.
- `react/prefer-ref-over-state` — Interaction state read only inside callbacks — use a ref, not state, to avoid re-renders.