Internal: React rules (derived state, effects only for outside systems, data hooks); read by work's executors before React code.
Resources
1Install
npx skillscat add idontlikedrinkpivo/web-dev-pipeline-codex-plugin/react Install via the SkillsCat registry.
Codex runtime. Before using this skill, read
../executor-catalog/references/codex-runtime.md. It defines tool mappings, named-agent dispatch and stage-chat orchestration; use it for inherited Claude Code terms below.
React
frontend says what a screen must do; this skill says how its React code stays changeable. The rules come from
react.dev (Rules of React, You Might Not Need an Effect, Choosing the State Structure) and from what an audit of a
real project found: one oversized page holding all its state and effects, network calls in components, a reference
list loaded several ways. Examples «так / не так»: references/examples.md.
The machine checks — keep them green
| Rule | Lint |
|---|---|
rendering is pure: no request, no outside change, no ref write, no setState during render |
react-hooks v7 recommended: purity, refs, set-state-in-render, immutability, globals |
no setState inside an effect to mirror other state |
set-state-in-effect |
hooks rules and complete effect dependencies; wanting to disable exhaustive-deps means the effect is wrong |
rules-of-hooks, exhaustive-deps |
| no component defined inside a component | static-components |
| a list key is a stable id from the data, not the index | @eslint-react/no-array-index-key |
no fetch or API client outside the data layer (shared/api, features/*/api.ts) |
no-restricted-imports, no-restricted-globals |
| no regex literal in a component — a format comes from the rules module | no-restricted-syntax |
no dangerouslySetInnerHTML outside the one sanitizer module |
the @eslint-react DOM rule against it (its name per the installed version), switched off only in that module |
| a function over 80 lines, a file over 500 — split | max-lines-per-function, max-lines |
tests query by role and label, use userEvent, findBy* instead of waitFor, no container or node access |
testing-library/* |
The reviewer checks — the machine cannot
- Do not store what you can compute. A value derived from props or state is computed during render; state
holds only what the user or the server gave. - An effect synchronizes with an outside system (a subscription, the DOM, a widget outside React) — never
reacts to its own state. What a user did is handled in the event handler. - Server data only through the data layer: one hook per read
operationId(TanStack Query), a mutation per
write; the component never fetches. A reference list (a dictionary) has one hook, shared by every screen. - Server data is not copied into
useState— except a form's initial values, reset withkey. - State is minimal and grouped: related values are one union object (
typescript→ «Impossible states are
unrepresentable») or auseReducer. More than four or fiveuseStatein a component is a sign it does two
jobs. - A page is a data hook
use<Screen>()plus a view (props in, JSX out); the view knows nothing of the API. - Reset state with
key, not with an effect watching a prop. useRefholds a DOM node or a value that does not affect what is shown.- Memoize only on a measurement (the Profiler) or when the value is an effect's dependency; with the React
Compiler on, do not memoize by hand. - Shared logic with hooks is a custom
use*hook, never a copy; shared Loading / Error / Empty are
components, chosen by the query's status. - A form is one library (React Hook Form) and one schema derived from the generated request type; a format
rule is imported from the one module the domain model's «Кто ещё применяет» names; field values are not
mirrored inuseState. - What the user may do (a button shown or not) comes from the response when the contract carries it,
never re-derived on the screen. - Variants by composition (
children, a separate component), not by yet another boolean prop. Context holds
what rarely changes (session, theme), never server data. - The browser keeps no secret and no session token. Text a user typed, or another user wrote, never reaches
dangerouslySetInnerHTML; rich text the product must show goes through one sanitizer (DOMPurify) in one
module, and a link built from such text gets its scheme checked (https:/mailto:only). AVITE_*orNEXT_PUBLIC_*variable is shipped to every visitor — an API key or a secret never goes there; a call that
needs one goes through the backend. A session token never goes intolocalStorageorsessionStorage; how
the session travels is the architecture §1 decision — the API'sHttpOnlycookie (credentials sent), or a
Bearer token, the backend skills' default, held in memory only. - Tests: Testing Library through
renderWithProvidersfrom the shared test folder, the network mocked by
MSW, the component's own hooks and modules never mocked, one behaviour per test (vitest).
React-version facts (ref as an ordinary prop, <Context> as its own provider) are points to check:frontend/references/version-checks.md → React major.