gap·map

React rendering

[ junior depth ]

What triggers a re-render in React

Two things, and only two: the initial mount, and a state setter called in the component or any ancestor. Note what's missing from that list: "props changed". Props trigger nothing by themselves; the parent re-rendered and passed new arguments. By default, a re-rendering component re-renders all of its children, whether their props changed or not.

Render ≠ DOM update

A render has three stages: trigger → render → commit. "Render" means React calls your function and compares the result with last time. The commit then touches only the DOM nodes whose output differs:

function Clock({ time }) {
  return (
    <>
      <h1>{time}</h1>
      <input />
    </>
  );
}

Clock re-renders every second, but only the <h1> text is updated. The <input> keeps whatever you typed, because its DOM node is never rebuilt. ❌ "Re-render = the DOM got recreated" fails interviews and causes fear-driven over-optimization.

State updates apply on the next render

const [count, setCount] = useState(0);
function onClick() {
  setCount(count + 1);
  console.log(count); // still 0!
}

setCount doesn't mutate count. It schedules a render where count will be 1. Inside the current event handler, the variable keeps its current value. This one fact explains half of all beginner React bugs.

Why list items need keys

Rendering a list requires key: unique among siblings, stable across renders. Use a data id, never Math.random(), and an index only if the list can never reorder or shrink. The middle notes cover what keys have to do with identity, and how React decides whether your component keeps its state.

[ middle depth ]

Where React state lives: position in the tree

The load-bearing rule of reconciliation: React ties state to the position in the tree plus the component type, not to the JSX tag you wrote and not to props:

{swapped ? <Counter label="B" /> : <Counter label="A" />}

Swap these and the count survives. Same type, same position, so React updates the existing instance in place (only label changes). Change the type at a position, or wrap it differently (<div><Counter/></div><section><Counter/></section>), and the whole subtree unmounts, state gone. Props never participate in this decision.

key is an identity override

key tells React "this is who the element is", overriding position:

<Chat key={selectedContact.id} contact={selectedContact} />

New contact → new key → React unmounts and remounts Chat, wiping the draft. This is the idiomatic "reset a form on switch" pattern, with no useEffect cleanup dance. It also explains why index-as-key breaks: delete the first row and every row's index shifts. State and DOM stay glued to positions while your data moved, so inputs "jump" between rows.

memo and the reference trap

memo(Component) skips a re-render only if every prop passes Object.is. Which means this defeats it every single render:

<Profile person={{ name: "Ann" }} onSave={() => save()} />  // ❌ fresh refs

Inline objects, arrays and functions are new references each render. The fixes: useMemo/useCallback to stabilize them, or pass primitives. Stabilizing references for a memo boundary or a dependency array is the real job of useMemo/useCallback; sprinkling them around makes nothing faster on its own.

One more classic in this family: ❌ defining a component inside another component. New function each render = new type at that position = full remount, state loss, every render.

Batching

Multiple setState calls in one event produce one render (since React 18, also in promises and timeouts). Three setCount(count + 1) calls still add 1, because each call reads the same captured value. Sequential updates need the updater form setCount(c => c + 1), which reads the queued value.

[ senior depth ]

How React reconciliation works, stated precisely

React's diff is a heuristic, not a minimal tree-edit algorithm: compare children by position; same element type → update in place (state and DOM preserved); different type → unmount the whole subtree; key replaces position as identity. Those four clauses are the entire rule set. Everything from "my count survived the swap" to "index keys corrupted my list" derives from them, so you can reason out any case instead of memorizing it.

When memo pays off

Memoization is a bet: you pay prop comparison on every render plus cache memory, to sometimes skip a render. It loses when props change every render anyway (inline refs), when the component is cheap, or when there's no memo boundary consuming your stabilized references. Two structural moves usually beat it:

  • Colocate state down. The re-rendering subtree is the component that owns the state. Move the input's state out of the page component and 90% of the tree stops re-rendering. First tool, always.
  • Pass children. A parent's own state change re-uses the same element reference for {children}, and React bails out of re-rendering it:
function Hover({ children }) {
  const [on, setOn] = useState(false);
  return <div onMouseMove={() => setOn(!on)}>{children}</div>;
}
// <Hover><Expensive /></Hover> — Expensive never re-renders on hover

Also know memo's limits: it only blocks parent-driven re-renders, while own state and consumed context go through. React Compiler auto-memoizes, so hand-written memo/useCallback is on its way to legacy status.

Purity is a contract with the scheduler

StrictMode double-invokes renders in dev to catch impurity. Interviewers probe this with "why does my log fire twice"; the double call is the intended behavior, and saying so is the whole answer. The deeper reason purity matters: with concurrent features, React may start a render, throw it away, and restart (transitions, Suspense). A render-phase side effect fires for renders that never commit. Interruptible rendering is only safe because renders can be discarded without consequence.

Fixing a slow re-render, in order

"Typing in the search box is slow." The fix is a sequence: measure (Profiler), colocate the state down, compose (children/JSX props), only then memo the hot boundary and stabilize its props, and for truly unavoidable work, useTransition/useDeferredValue. Memo sits fourth for a reason: applied before the structural moves, it papers over a tree shape that colocation or composition would have fixed for free, and it keeps costing comparisons on every render afterwards.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

unlocks

more React