React Coding Standards
The full reference for building React UIs at Finperiti: components, props, state, effects, hooks, data fetching, performance, forms, context, accessibility, and security. A few clear habits keep React working well: immutable state, correct effect dependencies, and composition. Without them you get stale data, infinite loops, and leaks. We write React in TypeScript; follow TypeScript Coding Standards and Gotchas: React alongside this.
We build function components with hooks, in strict TypeScript. Most React bugs come from misunderstanding rendering and state, so these standards cover the safe patterns in depth. Accessibility and security are part of the standard, not extras. The UI runs in a hostile browser and must work for everyone.
ESLint (including the rules-of-hooks and exhaustive-deps plugins) and Prettier enforce much of this. This page is the shared understanding and the conventions tools cannot enforce.
Components & composition
- DoWrite small, focused function components with typed props. Compose them instead of building large components or copy-pasting UI (see Frontend Architecture).
- DoKeep components mostly presentational. Move data fetching and business logic into hooks or a data layer so the UI stays testable and reusable.
- DoName components in PascalCase, one main component per file, and keep related files together (component, hook, styles, test).
- DoPrefer composition (children, render props, small components) over long prop lists and deep conditionals.
- AvoidHuge components that fetch data, hold state, contain business rules, and render everything at once.
Props & contracts
- DoType props precisely (no
any). Make required and optional clear, and model variants with unions, not loose flags (see TypeScript Coding Standards). - DoTreat props as read-only inputs owned by the parent. Never mutate them; request changes through callbacks.
- DoKeep prop lists focused. If a component takes many props, split it or group related props into an object.
- AvoidPassing props down many levels. Lift state or use context where it genuinely helps (see below).
State & immutability
- AlwaysTreat state as immutable. Produce new objects and arrays (spread, map, filter); never mutate. React compares references to decide what to re-render.
- DoUse the functional updater (
setCount(c => c + 1)) when the next state depends on the previous one, because updates are batched and async. - DoKeep state minimal and in the right place: local when only one component needs it, lifted only when it is genuinely shared.
- DoCompute values during render instead of storing them in state. Duplicated state drifts out of sync.
- DoUse
useReducerfor complex state changes, and keep state serialisable where possible.
items.push(newItem); // mutates the same array
setItems(items); // same reference, so React may not re-render
React compares references to decide what to re-render. The array is the same object, so the change can be missed.
setItems([...items, newItem]); // a new array reference
A new reference tells React the state changed, so it re-renders reliably.
Effects & lifecycle
- DoUse
useEffectonly to synchronise with the outside world (subscriptions, non-React APIs). Give it a correct dependency array and a cleanup function. - DoCancel or guard async work on unmount, so you do not set state on an unmounted component or leak (see Gotchas: React).
- DoCompute during render and handle user actions in event handlers. Many effects are unnecessary and cause extra renders.
- NeverSet state during render without a condition, or in an effect without the right dependencies. This is the classic infinite re-render loop.
useEffect(() => { setRows(rows); }); // no deps, so it loops
rows.push(newRow); // mutates state
{rows.map((r, i) => <Row key={i} ... />)} // index key
An effect with no dependency array runs on every render and loops. Mutating rows is not detected and can corrupt state. Index keys link the wrong rows when the list changes. Three classic React bugs.
useEffect(() => { const sub = api.subscribe(onMsg); return () => sub.close(); }, [api]);
setRows(prev => [...prev, newRow]); // new array
{rows.map(r => <Row key={r.id} ... />)} // stable id key
The effect synchronises with an external source and cleans up, state updates immutably with the functional form, and rows are keyed by a stable id.
Hooks rules & custom hooks
- AlwaysFollow the Rules of Hooks. Call hooks only at the top level of a component or hook, never in conditions, loops, or callbacks. The lint plugin enforces this.
- DoExtract reusable logic into custom hooks (
useXxx) with clear inputs and outputs. Keep them focused and testable. - DoKeep dependency arrays honest (exhaustive-deps). Do not fake them to silence the linter; fix the real dependency instead.
Data fetching
- DoPrefer a data-fetching library or framework loader over a hand-written fetch inside an effect. Handle loading, error, and empty states every time (see API & Contract Design).
- DoValidate API responses at runtime; do not trust the shape. Type them from the schema (see TypeScript Coding Standards, Trust Boundaries).
- DoAvoid request waterfalls (one fetch waiting on another) where you can run them in parallel. Cache on purpose.
Performance
- DoMeasure before optimising. Fix the big wins first: bundle size, code-splitting and lazy-loading, image size, and request waterfalls (see Frontend Performance).
- Consider
useMemo,useCallback, ormemoonly where profiling shows a real cost. Needless memoisation adds complexity for no gain. - AvoidPassing new object, array, or function literals as props to memoised children on every render. It defeats the memoisation.
- DoGive list items a stable, meaningful
key(an id, never the array index for dynamic lists). Virtualise very long lists.
save()} />
The object and function literals are new on every render, so the memoised child sees changed props and re-renders every time.
const style = useMemo(() => ({ margin: 8 }), []);
const onClick = useCallback(() => save(), [save]);
The references stay the same between renders, so the memoised child only re-renders when its props really change.
Context & shared state
- DoUse context for genuinely cross-cutting values that change rarely (theme, current user). Split contexts so unrelated updates do not re-render everything.
- ConsiderA dedicated state library only when the app state genuinely needs it. Do not centralise everything by habit.
- AvoidPutting fast-changing values in one global context. It causes wide re-renders.
Forms, accessibility & security
- DoBuild accessible UIs: semantic HTML (a real
<button>or<label>), keyboard support, labels on inputs, and enough contrast (see Accessibility, HTML & Markup Standards). - DoValidate form input in the UI for a good experience, and again on the server for trust. Show errors clearly and link them to the fields (see Trust Boundaries).
- DoLet JSX escape content by default. Keep auth tokens out of
localStorage(see Web & Frontend Security, Session & Token Management). - NeverPass untrusted content to
dangerouslySetInnerHTMLwithout sanitising it, and never rely on the client for security or authorization. The server enforces it; a hidden button is not access control (see Web & Frontend Security).
If userBio contains <script> or an onerror handler, it runs in the victim's session. This is a classic stored XSS. Render it as text, or sanitise it with a trusted library first.
{userBio} // React escapes the content automatically
JSX text rendering escapes HTML. The bio shows as plain text, and no injected markup runs.
Self-review checklist
- AskIs state updated immutably and kept in the right place, not duplicated?
- AskDo effects have correct dependencies and cleanup, with no infinite loop or leak, and do I follow the Rules of Hooks?
- AskIs the component focused and composable, with stable keys and handled loading and error states?
- AskIs it accessible and safe (escaped output, sanitised HTML, server-enforced auth, tokens not in localStorage)?