Skip to the solution
HardFrontend7 min read

How do you avoid re-renders with composition instead of memoization?

921 views

The problem

Question presented to candidate: "A component tree re-renders too much. Before you reach for memo and useCallback everywhere, what would you try structurally?"

What a strong answer should cover:

  • The mechanism: React bails out of re-rendering a subtree when the element object is referentially identical to the previous render. An element passed in as children is created by an ancestor that did not re-render, so it is identical.
  • Two structural moves: lift content up (pass the expensive subtree as children) and move state down (isolate the state into the smallest component that needs it).
  • Why this beats memoisation: no dependency arrays to get wrong, no useCallback chains, nothing to keep in sync as the code changes.
  • memo is fragile because it depends on every prop staying referentially stable; one inline object silently defeats it.
  • The important limitation: lifting content up only helps when the state lives in the component receiving children, not in an ancestor of it.
  • Where memoisation is still the right tool: long lists, expensive derived values, and props that genuinely must be objects.
  • The React Compiler changes the calculus for memoisation but not for composition β€” structure still matters.

Clarifying questions expected:

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

Code / implementation expected: Yes β€” the lift-content-up refactor, ideally with visible render counts.

  • react
  • performance
  • composition

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

7 min read Β· 9 sections

Try it in the playground

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

ReactThe same tree three ways: naive, memoised, and composed
import { useState, useCallback, memo } from "react";

const counts = { naive: 0, memoised: 0, composed: 0 };

function Expensive({ which }) {
  let x = 0;
  for (let i = 0; i < 150000; i++) x += i; // simulate real work
  counts[which]++;
  return (
    <p style={{ background: "#f6f6f6", padding: 8, borderRadius: 6, margin: "8px 0" }}>
      renders: <strong>{counts[which]}</strong>
    </p>
  );
}

const MemoExpensive = memo(Expensive);

// 1) NAIVE β€” the child element is recreated on every state change.
function Naive() {
  const [n, setN] = useState(0);
  return (
    <Panel title="Naive β€” child created inside">
      <button onClick={() => setN(n + 1)}>toggle ({n})</button>
      <Expensive which="naive" />
    </Panel>
  );
}

// 2) MEMOISED β€” works, but only because every prop happens to be stable.
//    Add one inline object here and it silently stops working.
function Memoised() {
  const [n, setN] = useState(0);
  const noop = useCallback(() => {}, []);
  return (
    <Panel title="Memoised β€” memo + useCallback">
      <button onClick={() => setN(n + 1)}>toggle ({n})</button>
      <MemoExpensive which="memoised" onThing={noop} />
    </Panel>
  );
}

// 3) COMPOSED β€” no memo anywhere. The element is created by Composed, which
//    never re-renders, so Shell receives the identical object every time.
function Shell({ children }) {
  const [n, setN] = useState(0);
  return (
    <Panel title="Composed β€” passed as children">
      <button onClick={() => setN(n + 1)}>toggle ({n})</button>
      {children}
    </Panel>
  );
}
function Composed() {
  return (
    <Shell>
      <Expensive which="composed" />
    </Shell>
  );
}

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

export default function App() {
  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6 }}>
      <Naive />
      <Memoised />
      <Composed />
      <p style={{ color: "#666", fontSize: 13 }}>
        Click each toggle several times. The first counter climbs on every click.
        The second and third both stay at 1 β€” but only the third achieves it
        structurally, with no memo, no useCallback, and nothing to maintain.
      </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 79 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 nextHard

What is `useDeferredValue` and how does it differ from debouncing?

Continue