React Correctness Rules
Rule
Scope: React and Next.js apps. Pass to daniel-product-engineer, lee-nextjs-engineer, security-engineer, and senior-engineer during audits.
Keys & Rendering
- MUSTUse stable unique IDs as
key, not array index — index keys cause state bugs on reorder/insert/delete- Exception: Static lists generated from
Array.from()placeholders where items never reorder
- Exception: Static lists generated from
- MUST NOTUse
&&with numbers for conditional rendering —{count && }renders0- FIX:
{count > 0 ? : null}
- FIX:
State & Effects
The guiding question: does this code run because the component was displayed, or because of a specific user interaction? Only the former belongs in an Effect. If no external system is involved, you probably don't need an Effect.
Derived state — compute during render, not in effects
- MUST NOTUse
useEffect+setStateto compute values from other state/props — calculate inline:// Wrong — unnecessary effect + extra re-render const [fullName, setFullName] = useState(''); useEffect(() => setFullName(`${first} ${last}`), [first, last]); // Right — derive during render (no state, no effect, no memo) const fullName = `${first} ${last}`; - SHOULDOnly reach for
useMemowhen the computation is genuinely expensive (≥1ms measured withconsole.time):const visibleTodos = useMemo( () => getFilteredTodos(todos, filter), [todos, filter], );
Reset state with key, not effects
- MUST NOTClear state in an effect when a prop changes — causes a flash of stale UI before the effect fires:
// Wrong — renders stale comment, then clears useEffect(() => { setComment(''); }, [userId]); // Right — React resets all state when key changes <Profile userId={userId} key={userId} />
Adjust partial state by storing IDs
- MUST NOTReset a selection in an effect when items change:
// Wrong useEffect(() => { setSelection(null); }, [items]); // Right — store the ID, derive the object during render const [selectedId, setSelectedId] = useState(null); const selection = items.find(item => item.id === selectedId) ?? null;
Event handler logic belongs in event handlers
- MUST NOTMove shared logic into an effect because multiple handlers need it — extract a plain function instead:
// Wrong — runs on every state change, not just user actions useEffect(() => { if (product.isInCart) showNotification('Added!'); }, [product]); // Right — call from each handler function buyProduct() { addToCart(product); showNotification('Added!'); } - MUST NOTRoute form submissions or mutations through state + effect — call the API directly from the event handler:
// Wrong const [payload, setPayload] = useState(null); useEffect(() => { if (payload !== null) post('/api/register', payload); }, [payload]); // Right function handleSubmit() { post('/api/register', formData); }
No effect chains (dominoes)
- MUST NOTChain effects where each sets state that triggers the next — consolidate into one event handler:
// Wrong — three effects, three extra renders useEffect(() => { if (card?.gold) setGoldCardCount(c => c + 1); }, [card]); useEffect(() => { if (goldCardCount > 3) { setRound(r => r + 1); setGoldCardCount(0); } }, [goldCardCount]); useEffect(() => { if (round > 5) setIsGameOver(true); }, [round]); // Right — derive what you can, consolidate the rest const isGameOver = round > 5; function handlePlaceCard(nextCard) { // all game logic in one handler }
Notify parents in handlers, not effects
- MUST NOTCall an
onChangecallback inside an effect after updating local state — causes an extra render cycle:// Wrong useEffect(() => { onChange(isOn); }, [isOn, onChange]); // Right — update both in the same handler function handleToggle() { const next = !isOn; setIsOn(next); onChange(next); } - SHOULDPrefer lifting state to the parent entirely (fully controlled component) over syncing via callbacks.
Parent owns data, passes it down
- MUST NOTFetch data in a child, then send it up via a callback effect — this inverts React's data flow:
// Wrong function Child({ onFetched }) { const data = useSomeAPI(); useEffect(() => { if (data) onFetched(data); }, [onFetched, data]); } // Right — parent fetches, passes down as props
External store subscriptions
- MUSTUse
useSyncExternalStorefor subscribing to browser APIs or third-party stores — not manualaddEventListenerin an effect:// Wrong useEffect(() => { const update = () => setIsOnline(navigator.onLine); window.addEventListener('online', update); window.addEventListener('offline', update); return () => { /* cleanup */ }; }, []); // Right const isOnline = useSyncExternalStore( subscribe, () => navigator.onLine, () => true, // server snapshot );
App initialization
- SHOULDRun one-time startup logic at module scope, not in a component effect (Strict Mode runs effects twice):
// Right — runs once at module load if (typeof window !== 'undefined') { checkAuthToken(); loadDataFromLocalStorage(); }
General state rules
- SHOULDConsolidate 3+ related
useStatecalls intouseReduceror a single state object - SHOULDBatch multiple
setStatecalls in one handler into a single update (React 18+ batches automatically in most cases, but verify in async callbacks) - MUST NOTFetch data in
useEffect— use React Query, SWR, or server components instead
Naming & Structure
- SHOULDName event handlers by what they do, not what they handle:
- Wrong:
handleClick,handleChange - Right:
handleSubmitOrder,handleUpdateQuantity
- Wrong:
- SHOULDBreak components over 250 lines into smaller focused components
- MUST NOTUse inline render functions in JSX (
{renderItems()}) — extract to a named component- Inline render functions don't get their own reconciliation boundary, causing unnecessary re-renders of siblings
Security
- MUST NOTUse
eval(),new Function(), or string-argumentsetTimeout/setInterval— code injection risk - MUST NOTHardcode secrets in source — detected by variable name patterns:
API_KEY,SECRET,TOKEN,PASSWORD,PRIVATE_KEY- FIX: Use environment variables via
process.envor.envfiles
- FIX: Use environment variables via
- MUSTFlag every
dangerouslySetInnerHTMLusage for security review — ensure input is sanitized (DOMPurify or equivalent)
Server Components & Actions (Next.js)
- MUST NOTMake client components async —
"use client"components cannot be async (they return a Promise, not JSX) - MUSTAdd auth check at the top of every server action —
auth(),getSession(), or equivalent before any data mutation - SHOULDUse
next/imageinstead of<img>— provides automatic optimization, lazy loading, and layout shift prevention - SHOULDUse
next/linkinstead of<a>for internal navigation — enables client-side transitions and prefetching - SHOULDUse
next/fontinstead of<link>Google Font tags — eliminates layout shift from font loading
Error Handling
- MUSTWrap async operations in try/catch with meaningful error handling — no empty catch blocks
- SHOULDAdd error boundaries around feature sections that can fail independently
- MUST NOTSilently swallow errors — at minimum log them; prefer surfacing to the user