Misconceptions: The Master List
What you'll learn
- The two dozen most common React myths, each with the reality and the proof
- A single reference to screenshot, share, and settle arguments with
- Micro-experiments you can run to verify any of these yourself
Every chapter of this series ended with a misconceptions section. This chapter collects the greatest hits, plus a few new ones, in one place, compressed to their essence: the myth, the reality, and the one-line proof or experiment that settles it. Bookmark this page. You will meet every one of these in code reviews, interviews, and your own 2 a.m. debugging sessions.
How to read this list
Each entry is self-contained and follows the same shape: Myth (what people believe), Reality (what's actually true), Proof/why (the experiment or mechanism that settles it). The proofs are deliberately runnable, most are one or two lines of code you can paste into any sandbox. Don't just read them; the ones that sting are the ones you believed yesterday, and those deserve a hands-on verification. Cross-references point back to the part of the series that covers the full mechanism.
State and rendering
1. Myth: setState is asynchronous, like setTimeout, it happens "later, eventually."
Reality: updates are scheduled and batched, not async. React processes them at a well-defined point: when the current execution context finishes.
Proof/why: flushSync(() => setCount(1)); console.log(div.textContent) reads the updated DOM on the very next line, impossible if the update were truly async. "Async" was always "batched" (Part 3).
2. Myth: setState(newValue) mutates the state variable to hold newValue.
Reality: state is a per-render snapshot; setState requests a re-render where the state variable will have the new value. The current render's variable never changes.
Proof/why: setCount(count + 1); console.log(count) logs the old value, the const binding in this render is untouched. Nothing was mutated; a new render was ordered (Part 3).
3. Myth: Mutating state directly works fine if you also call a force-update to trigger a render.
Reality: it "works" only until anything compares identities: memo bails out on the same object, useEffect deps see no change, time-travel debugging breaks, and concurrent renders can read your half-mutated object.
Proof/why:
state.items.push(x); // same array identity — invisible to every compare
forceUpdate(); // DOM updates, but memoized children don't:
// shallow compare passes, bail out, stale UI
Mutation poisons every identity-based optimization in the pipeline (Part 8).
4. Myth: A re-render means the DOM changes.
Reality: renders produce descriptions; the diff often finds zero changes, and the commit is then empty.
Proof/why: setCount((c) => c) (or any update yielding identical output) runs your component but performs zero DOM mutations, checkable in the Profiler, which shows a render with no committed changes (Parts 2 and 8).
5. Myth: Updating a ref triggers a re-render.
Reality: refs are mutable boxes outside the rendering system. Writing ref.current schedules nothing.
Proof/why: ref.current++ in a click handler changes no state and no screen, the value only appears if some other update happens to render. That's the entire point of refs (Part 3).
6. Myth: Batching only works inside React event handlers.
Reality: since React 18, batching applies everywhere: timeouts, promises, fetch callbacks, native event listeners.
Proof/why: two setState calls inside setTimeout(..., 0) produce one render, not two, add console.count('render') and see (Part 3). Pre-18, that logged two renders; the myth is a fossil of that era.
Effects
7. Myth: useEffect(fn, []) is componentDidMount.
Reality: it's "synchronize once after the first commit", a different concept. Effects are for syncing with external systems, not lifecycle ceremony; they also re-run on remounts, StrictMode drills them setup→cleanup→setup, and treating them as mount hooks leads to missing-cleanup bugs.
Proof/why: under StrictMode in dev, useEffect(fn, []) visibly runs setup twice on mount (chapter 1 of this part). componentDidMount never did that, because this isn't that.
8. Myth: React "watches" the dependency array and fires the effect when a dep changes.
Reality: nothing is watched. On every render, React compares the new dep array against the previous one with Object.is; the effect runs if any value differs. No render, no comparison, no effect.
Proof/why: mutate a dep in place (dep.value = 2) without re-rendering, the effect never fires. Watching would have caught it; per-render comparison can't (Part 3).
9. Myth: useEffect runs before the browser paints.
Reality: effects run after paint, asynchronously, that's why they're the default. The before-paint hook is useLayoutEffect, and it blocks painting.
Proof/why: in an effect, read a layout property and change it, you'll see a flicker the layout effect version doesn't have; conversely, heavy work in a layout effect visibly delays first paint (Part 3).
Performance
10. Myth: React.memo makes apps faster by default, wrap everything.
Reality: memo is a bet that comparing props is cheaper than rendering the subtree. On tiny components the compare ≈ the render; with unstable props the compare always fails. Universal memo can be a net loss.
Proof/why: pass an inline object (style={{...}}) into a memoized component and count renders: it renders every time anyway, and you paid for the compare too (Part 8).
11. Myth: useMemo and useCallback are free performance.
Reality: both allocate cache slots, run dependency comparisons every render, and retain old values in memory. They pay off only when they protect a real cost: an expensive computation or a memo/effect boundary that must see stable identity.
Proof/why: wrap a string concatenation in useMemo, you've added machinery to guard microsecond work. The Profiler shows no render time saved (Part 8).
12. Myth: "Children re-render when the parent does" is a problem you fix with memo.
Reality: the first fixes are colocation (move the state down) and composition (pass content as children from a stable grandparent). Memo is the third tool, for what remains.
Proof/why:
function Layout({ children }) {
const [open, setOpen] = useState(true); // state lives here…
return <section>{open && children}</section>; // …children don't re-render
}
// <Layout><Chart /></Layout> — Chart's element was created by the
// grandparent: same object every time, same-element bailout.
The full walkthrough is Part 8, chapter 1.
13. Myth: React is slow at DOM updates. Reality: React's DOM commits are minimal and batched; the tradeoff is the render/diff work before the DOM. When React apps feel slow, it's almost always wasted render work or huge trees, not DOM slowness. Proof/why: a committed update to one text node is one DOM mutation, same as hand-written JS. Profile a "slow" app: the time is in render, not commit (Part 8).
Identity and keys
14. Myth: Index keys are fine for static lists. Reality: mostly true, and the nuance matters. For a list that never reorders, inserts, or deletes, index keys behave identically to id keys. The moment items can move, index keys mis-preserve state (row 3's input state stays at position 3 when the item moved to position 1). Proof/why: render inputs keyed by index, type in each, then prepend an item: the text stays at its old position, now labeling the wrong item. Id keys move the state with the item (Part 2).
15. Myth: Keys exist to improve performance.
Reality: keys control identity, which instance's state survives across renders. Any performance effect is a side effect of identity matching correctly.
Proof/why: <Editor key={docId} /> doesn't make anything faster; it makes the editor reset when the document changes. Keys are a correctness tool (Part 2).
16. Myth: Context is a state manager.
Reality: context is a delivery mechanism, it passes a value down without prop drilling. It holds no state, has no update logic; the state lives in whatever useState/useReducer produced the value you passed in.
Proof/why: <Ctx.Provider value={value}> with a changing value re-renders every consumer on every change, no selectors, no subscriptions to slices. A state manager's job (fine-grained updates) is exactly what context doesn't do (Parts 3 and 8).
Concurrency, Suspense, and boundaries
17. Myth: Concurrent rendering means React renders in parallel threads. Reality: it's one thread, interruptible. Concurrency here means React can pause a render, handle something urgent, and resume or discard, not that two renders run simultaneously. Proof/why: your component code never needs locks or synchronization; a render may be thrown away mid-flight (which is why render must be pure), but it never races another render (Part 4).
18. Myth: Transitions make rendering faster.
Reality: transitions make the UI responsive; total work is the same or slightly more. The urgent update (typing) renders immediately at high priority; the expensive one (filtering 10,000 rows) renders later, interruptibly.
Proof/why: wrap a huge filter in startTransition, the input stays snappy, but the list takes just as long to compute. Nothing got faster; things got ordered (Part 4).
19. Myth: Suspense fetches your data.
Reality: Suspense only displays pending and error states for trees that aren't ready. Fetching is done by your framework, router, or cache, which communicates readiness by throwing promises to the boundary.
Proof/why: <Suspense fallback={...}> around a component that does its own useEffect fetching shows the fallback never, nothing threw a promise, so Suspense had nothing to catch (Part 5).
20. Myth: Error boundaries catch everything.
Reality: boundaries catch errors during rendering, in lifecycle, and in constructors of children below them. They do not catch errors in event handlers, async callbacks, timers, or errors thrown by the boundary itself.
Proof/why: onClick={() => { throw new Error('nope') }} crashes past every boundary, handlers run outside render; use try/catch there (Part 5).
Portals, SSR, and dev tooling
21. Myth: Portals break event bubbling, events are trapped or leak oddly because the DOM is elsewhere.
Reality: events bubble through the React tree, not the DOM tree. A portal's DOM node lives outside the parent <div>, but its events bubble to the parent component exactly as if it were inside.
Proof/why: wrap a modal portal in a parent with onClick, clicks inside the modal still reach the parent's handler. Bubbling follows fibers, not nodes.
22. Myth: An SSR page is interactive as soon as it appears. Reality: there's a hydration gap: HTML paints first, but event handlers attach only after the JS loads and React hydrates the tree. Clicks in between can be lost. Proof/why: throttle your network, load an SSR page, and click a button the instant it appears, nothing happens until hydration completes. This gap is what partial/selective hydration exists to shrink (Part 7).
23. Myth: React Server Components are just SSR with a new name.
Reality: different, composable things. SSR renders client components to HTML per request, then hydrates. RSC renders components exclusively on the server, their code never ships to the browser, into a serialized description the client merges. You can use RSC without SSR (build-time) and SSR without RSC; together they compose.
Proof/why: a server component can await db.query(...) directly and its npm imports add zero bytes to the client bundle, impossible for any SSR'd client component, which must ship and re-run in the browser (Part 7).
24. Myth: StrictMode double-rendering means something is wrong with my code, or with React. Reality: the double-invocation is the detector working as designed; only visible doubling of effects (doubled logs, timers, mutations) indicates a bug, one that already existed. Proof/why: pure components under StrictMode behave identically except for the discarded second call. If doubling breaks something, that something was impure and would have broken under concurrent rendering anyway (chapter 1 of this part).
Diagram
Rendered diagram (PNG hi-res):
Rendered diagram (PNG hi-res):
Why these myths persist
- Old truths fossilize. "Batching only in event handlers" and "setState is async" were true or true-ish in React 16. Documentation ages; the mental models built on it age slower.
- Names lie politely.
useEffectsounds like lifecycles,keysounds like an optimization hint,Suspensesounds like a fetcher. The API names describe the what; the myths fill in a wrong how. - Surface behavior mimics the myth. A mutated object plus a forced render looks fine, until memo, effects, or concurrency expose the poisoned identity. Myths survive exactly as long as their failure mode stays hidden.
- The real model is one level deeper than the API. Every myth here dissolves once you hold the actual machinery: snapshots per render, identity-based comparison, interruptible single-threaded rendering, trees over DOM. Which is, not coincidentally, this entire series.
Try it yourself
- Pick any three myths you believed last month. Run their "proof" experiments verbatim and watch the myth die with your own console.
- Settle the async question permanently:
flushSync(() => setCount(1)); console.log(ref.current.textContent), then explain the result to a teammate without using the word "async". - Reproduce the index-key bug (myth 14): inputs keyed by index, type names, prepend an item, watch state stick to position. Fix with id keys and re-verify.
- Prove myth 21 wrong in a sandbox: portal a
<button>outside the root div, putonClickon an ancestor component, click, and observe the event arrive.
Recap
setStateis batched scheduling, not async; it requests a render, it never mutates the current snapshot.- Mutation "works" only until anything compares identities, which is everything: memo, deps, diffing.
- Effects compare deps per render, run after paint, and are synchronization, not lifecycle methods.
- memo,
useMemo, anduseCallbackare bets with costs; colocation and composition come first. - Keys control identity, not performance. Context delivers values; it manages nothing.
- Concurrency is interruptibility on one thread; transitions reorder work rather than removing it.
- Suspense shows pending states, boundaries catch render-phase errors, portals bubble through the React tree, SSR hydrates (with a gap), RSC is not SSR, and StrictMode doubles your code, not your bugs.
Next
Glossary