#How to apply this file
Each section opens with one imperative line; apply every rule in the section it introduces. Do not summarise or skip a section.
#Purpose
Rules for writing React components. Most React bugs are not rendering bugs — they are state-modelling bugs: state that should have been derived, state duplicated in two places, or an effect synchronising something that did not need synchronising.
Hooks discipline is Frontend/hooks; global state is
Frontend/state-management.
#Derive, do not store
[INST] Apply every rule in this section: Derive, do not store. [/INST]
tsx// Two sources of truth — they will diverge
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0); // must be updated everywhere items is
// One source. `total` cannot be stale by construction.
const [items, setItems] = useState([]);
const total = items.reduce((s, i) => s + i.priceCents * i.qty, 0);
Before adding state, ask whether it can be computed from props, existing state, or
the URL. Only add useMemo if that computation is measurably expensive.
Keep state at the lowest common owner of the components that read it. Lifting state higher than necessary re-renders subtrees that do not care.
Model impossible states out of existence:
tsx// Permits { loading: true, error: Error, data: Data } — meaningless
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
// One value; illegal combinations cannot be represented
type State =
| { status: "idle" } | { status: "loading" }
| { status: "error"; error: Error } | { status: "success"; data: Data };
#Most effects are unnecessary
[INST] Apply every rule in this section: Most effects are unnecessary. [/INST]
useEffect synchronises with something outside React: the DOM, a
subscription, a timer, an analytics SDK. It is not a general-purpose "run this
after render" hook.
| Instead of an effect | Do this |
|---|---|
| Computing derived data | Calculate during render |
| Resetting state when a prop changes | Change the key so React remounts |
| Handling a user action | Do it in the event handler |
| Fetching data | Use a data library or a framework loader |
| Syncing two pieces of state | Remove one of them |
tsx// Effect chain: renders twice, and the intermediate state is visible
useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]);
// Just compute it
const fullName = `${first} ${last}`;
When you do use an effect: include every referenced value in the dependency array, return a cleanup function, and handle the fact that it may run twice in development Strict Mode — which is a bug detector, not a bug.
Fetching in useEffect is where race conditions live: two requests, the slower
one resolving last and overwriting fresh data. Use an AbortController and ignore
stale results, or use a library that already does. → Frontend/hooks
#Keys are identity, not position
[INST] Apply every rule in this section: Keys are identity, not position. [/INST]
tsx// Index keys: deleting the first item makes React reuse the wrong DOM node.
// Input values, focus and scroll position follow the index, not the item.
{items.map((item, i) => <Row key={i} item={item} />)}
// Stable identity
{items.map((item) => <Row key={item.id} item={item} />)}
Index keys are safe only for a list that is never reordered, filtered, or prepended to. Since that is rarely guaranteed, use the id.
The same mechanism is a feature: changing the key on a component discards its
state and remounts it — the correct way to reset a form when the selected record
changes.
#Memoise on evidence
[INST] Apply every rule in this section: Memoise on evidence. [/INST]
memo, useMemo and useCallback are not free: they add allocation, comparison
cost and code that must stay correct.
- Profile first with the React DevTools Profiler. Optimise the component that actually shows up.
- The React Compiler handles most memoisation automatically. If it is enabled, manual memoisation is usually noise.
useMemofor a genuinely expensive computation or a referentially-stable value passed to a memoised child — not for{a: 1}.- Composition often beats memoisation: passing
childrenthrough means the parent re-rendering does not re-render them.
Never memoise to fix an infinite loop. That is a dependency bug; fix the dependency.
#Rendering untrusted content
[INST] Apply every rule in this section: Rendering untrusted content. [/INST]
tsx// React escapes this automatically — safe
<div>{userComment}</div>
// This bypasses every protection React gives you
<div dangerouslySetInnerHTML={{ __html: userComment }} />
If you must render HTML, sanitise it with DOMPurify on a strict allowlist,
server-side where possible.
Also unsafe: href={userUrl} permits javascript: — validate the scheme.
<script src={userValue}> and style={{ background: userValue }} are equally
injectable. → Security/xss
#Accessibility is not optional
[INST] Apply every rule in this section: Accessibility is not optional. [/INST]
- Semantic elements first:
<button>,<a href>,<nav>,<main>. A<div onClick>is not keyboard-reachable and is invisible to a screen reader. - Every input has a
<label>associated byhtmlFor. - Focus must be visible and managed: on route change, on modal open, and returned on close.
- ARIA is a last resort. A correct native element needs none.
- Images have
alt; decorative images havealt="". - Test with a keyboard only, and run
axein CI. →Testing/accessibility
#Anti-patterns
[INST] Apply every rule in this section: Anti-patterns. [/INST]
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| State duplicating derived data | Two sources of truth diverge | Compute during render |
| Boolean flags for a state machine | Permits impossible combinations | A discriminated union |
| State lifted too high | Re-renders unrelated subtrees | Lowest common owner |
| Effect to compute derived state | Extra render; visible intermediate state | Compute during render |
| Effect to reset state on prop change | Runs after paint; flashes | Change the key |
| Fetching in an effect without cleanup | Race conditions overwrite fresh data | AbortController or a data library |
| Missing effect dependencies | Stale closures capture old values | Exhaustive deps lint rule |
| Index as key | Wrong DOM reused; input state follows position | Stable id |
| Memoising everything | Cost with no measured benefit | Profile first |
| Memoising to stop a loop | Hides a dependency bug | Fix the dependency |
| Mutating state directly | React does not re-render | Replace, never mutate |
dangerouslySetInnerHTML with user content | XSS | Sanitise on an allowlist |
Unvalidated href from input | javascript: execution | Validate the scheme |
<div onClick> | Not keyboard or screen-reader accessible | <button> |
| Business logic inside components | Untestable without rendering | Extract to functions |
#Checklist
- Verify: No state stores what can be derived from other state or props
- Verify: State lives at the lowest common owner of its consumers
- Verify: Related state is modelled so impossible combinations cannot exist
- Verify: Effects are used only to synchronise with systems outside React
- Verify: Derived values are computed during render, not in effects
- Verify: State resets are done with
key, not effects - Verify: Every effect has exhaustive dependencies and a cleanup function
- Verify: Data fetching cancels or ignores stale responses
- Verify: List keys are stable identities, never array indices
- Verify: Memoisation is applied only where profiling showed a cost
- Verify: State is replaced, never mutated
- Verify:
dangerouslySetInnerHTMLis unused, or the input is sanitised - Verify: URLs from user input are scheme-validated
- Verify: Interactive elements are semantic and keyboard-reachable
- Verify: Every input has an associated label; focus is managed on navigation
- Verify: Business logic lives outside components and is unit-tested