Skip to solution
hardFrontend

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

921 views
01

Understand 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.

reactperformancecomposition
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 senior React interviews — assumes memo and re-render basics. Difficulty: Hard

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

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

The same tree three ways: naive, memoised, and composed
Run Playground
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>
  );
}
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 79 of 119 decoded in the React.js track. One more won't hurt.

Back to track