Skip to the solution
MediumFrontend6 min read

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

706 views

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.

  • performance
  • optimization
  • react.memo
  • hooks

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 10 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactThe same tree fixed three ways β€” state down, content up, and memo
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 52 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

How do you handle forms in React?

Continue