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:
$ 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**/*.jsxsrc/**/*.tsapp/**/*.tspackages/**/src/**/*.tsapps/**/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 id | Severity | Catches |
|---|---|---|
react/effect-derived-state | BLOCKER | `useEffect` for derived state. State computable from existing props/state must be computed during render, never synced via effect. |
react/effect-for-event-logic | BLOCKER | `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 | BLOCKER | Missing `useEffect` cleanup for subscriptions, timers, listeners, and in-flight requests (AbortController). |
react/direct-state-mutation | BLOCKER | Direct state mutation (`arr.push`, `obj.x =`, `.splice`) followed by `setState` — silent update failure. |
react/conditional-hook-calls | BLOCKER | Conditional hook calls — hooks inside `if`, loops, or early returns. |
react/key-is-index | BLOCKER | `key={index}` on lists that reorder, insert, or delete. |
react/server-data-in-local-state | BLOCKER | 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 | BLOCKER | Component defined inside another component — remounts every render, loses state. |
react/useformstatus-placement | BLOCKER | `useFormStatus` in the same component that renders the `<form>` — always returns `pending: false`; must be in a child. |
react/promise-in-render | BLOCKER | Promise created during render passed to `use()` — infinite loop; promise must come from props, state, or cache. |
react/exhaustive-deps-disabled | BLOCKER | `eslint-disable react-hooks/exhaustive-deps` — hides stale-closure bugs; require refactor instead. |
react/incomplete-dependency-array | HIGH | Incomplete dependency arrays (stale closures). |
react/sequential-awaits | HIGH | Sequential `await`s for independent operations — require `Promise.all`. |
react/barrel-file-imports | HIGH | Barrel-file imports (`import { X } from '@/components'`) — require direct imports; barrels bloat bundles and cause circular deps. |
react/heavy-component-static-import | HIGH | Heavy components (charts, editors, modals) imported statically — require `next/dynamic` / `React.lazy`. |
react/uncontrolled-to-controlled | HIGH | `useState(undefined)` for controlled inputs — uncontrolled→controlled warning; use `''`. |
react/missing-error-boundary | HIGH | Missing Error Boundary around subtrees that fetch or can throw. |
react/falsy-and-rendering | HIGH | `&&` conditional rendering with a possibly-falsy non-boolean left side (`count && <X/>` renders `0`) — require ternary or explicit boolean. |
react/trivial-memoisation | SUGGESTION | `useMemo`/`useCallback` wrapping trivial primitives — remove. |
react/missing-lazy-initialiser | SUGGESTION | Expensive `useState` initializer without lazy init — pass a function. |
react/starttransition-for-non-urgent | SUGGESTION | `startTransition` for non-urgent updates driving expensive renders. |
react/oversized-component | SUGGESTION | Component over ~300 lines — suggest split. |
react/prop-drilling | SUGGESTION | Prop drilling beyond 2–3 levels — suggest composition or context. |
react/prefer-ref-over-state | SUGGESTION | Interaction 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:
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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
# 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.