Skip to solution
mediumFrontend

How do you prevent unnecessary re-renders in functional components?

706 views
01

Understand the problem

Question presented to candidate: "A component tree re-renders more than it should. Walk me through how you would reduce that."

What a strong answer should cover:

  • Measure first. The DevTools Profiler with "record why each component rendered" tells you the cause — props, state, parent, or context — and that determines the fix.
  • Know the four causes: its own state changed, its parent re-rendered, a context it reads changed, or its props changed.
  • Structural fixes before memoisation: move state down (colocate it in the smallest component that needs it) and lift content up (pass the expensive subtree as children).
  • Why structure first: it cannot be accidentally switched off, whereas memo is silently defeated by one inline object.
  • Then React.memo — and the caveat that it needs useMemo/useCallback cooperation from the parent to work at all.
  • Context needs its own fix: split by change frequency, since memo does not block context propagation.
  • Long lists want virtualisation, not memoisation — rendering 20 rows instead of 10,000.
  • The honest framing: a re-render is not automatically a problem. It is cheap unless the component is expensive or it happens very often.
  • The React Compiler automates the memoisation half.

Clarifying questions expected:

  • "Have we profiled, and is the re-render actually expensive or just frequent?"
  • "Where does the state that triggers it live?"

Code / implementation expected: Yes — the move-state-down and lift-content-up refactors with visible counts.

performanceoptimizationreact.memohooks
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes memo and hooks. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The render counts quoted below were measured on React 19.2.8 across this collectio

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

The same tree fixed three ways — state down, content up, and memo
Run Playground
import { useState, useCallback, memo } from "react";

const counts = { naive: 0, stateDown: 0, contentUp: 0, memoised: 0 };

function Expensive({ which }) {
  let waste = 0;
  for (let i = 0; i < 120000; i++) waste += i;   // simulate real render cost
  counts[which]++;
  return (
    <p style={{ background: "#f6f6f6", padding: 6, borderRadius: 6, margin: "6px 0", fontSize: 14 }}>
      renders: <strong>{counts[which]}</strong>
    </p>
  );
}

// ❌ NAIVE: the input state sits beside the expensive child, so every
//    keystroke re-renders it.
function Naive() {
  const [q, setQ] = useState("");
  return (
    <Panel title="❌ state beside the expensive child">
      <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="type here" />
      <Expensive which="naive" />
    </Panel>
  );
}

// ✅ FIX 1 — MOVE STATE DOWN: the input owns its own state, so renders
//    cannot travel sideways to the sibling.
function SearchBox() {
  const [q, setQ] = useState("");
  return <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="type here" />;
}
function StateDown() {
  return (
    <Panel title="✅ state moved down into the input">
      <SearchBox />
      <Expensive which="stateDown" />
    </Panel>
  );
}

// ✅ FIX 2 — LIFT CONTENT UP: the element is created by ContentUp, which never
//    re-renders, so Shell receives the identical object every time.
function Shell({ children }) {
  const [q, setQ] = useState("");
  return (
    <Panel title="✅ expensive child passed as children">
      <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="type here" />
      {children}
    </Panel>
  );
}
function ContentUp() {
  return <Shell><Expensive which="contentUp" /></Shell>;
}

// ✅ FIX 3 — MEMO: works, but only while every prop stays referentially stable.
const MemoExpensive = memo(Expensive);
function Memoised() {
  const [q, setQ] = useState("");
  const noop = useCallback(() => {}, []);
  return (
    <Panel title="✅ memo + useCallback (fragile)">
      <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="type here" />
      <MemoExpensive which="memoised" onThing={noop} />
    </Panel>
  );
}

function Panel({ title, children }) {
  return (
    <section style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, marginBottom: 10 }}>
      <h4 style={{ margin: "0 0 8px", fontSize: 14 }}>{title}</h4>
      {children}
    </section>
  );
}

export default function App() {
  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 520 }}>
      <Naive />
      <StateDown />
      <ContentUp />
      <Memoised />
      <p style={{ color: "#666", fontSize: 13 }}>
        Type in each box. The first counter climbs with every keystroke; the
        other three stay at 1. Only the middle two achieve it structurally —
        add an inline object prop to the memo version and it climbs again.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 52 of 119 decoded in the React.js track. One more won't hurt.

Back to track