Skip to solution
mediumFrontend

What is 'prop drilling' and how can it be avoided?

552 views
01

Understand the problem

Question presented to candidate: "A value from the top of your app is needed six levels down. What are your options?"

What a strong answer should cover:

  • Prop drilling is passing a prop through components that do not use it, purely to reach a descendant.
  • It is not automatically wrong — two levels is explicit and readable. It becomes a problem past three or four, when intermediate components take props solely to forward them.
  • The real costs: you can no longer see which components actually depend on a value; renaming means touching every level; and the intermediate components have a wider interface than their job needs.
  • The escape ladder, in order: composition (children or element slots), then Context, then a store.
  • Composition first is the point most candidates miss — restructuring so the deep component is created higher up removes the threading entirely, needs no new machinery, and has a measurable re-render benefit.
  • Context is a transport, not a state manager; every consumer re-renders when the value changes.
  • A store (Redux, Zustand, Jotai) adds selector subscriptions, so only components reading the changed slice re-render.
  • Custom hooks can hide the useContext call so consumers do not import the context directly.

Clarifying questions expected:

  • "How many levels, and do any of the intermediate components use the value?"
  • "How often does it change, and how many components read it?" — that decides Context versus a store.

Code / implementation expected: Yes — the drilled version and the composition refactor side by side.

prop drillingcontext apistate managementperformance
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 props and components. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The re-render figures quoted in section 4 were measured on React 19.2.8; see

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

The drilled version, then the same tree fixed by composition
Run Playground
import { useState } from "react";

const renders = { drilledMid: 0, composedMid: 0 };

// ── DRILLED: Layout and Panel take the user prop only to hand it down. ────
function DrilledLayout({ user, children }) {
  renders.drilledMid++;
  return <Frame title="Drilled">{children}<DrilledPanel user={user} /></Frame>;
}
function DrilledPanel({ user }) {
  // Does not use user either — pure courier.
  return <DrilledBadge user={user} />;
}
function DrilledBadge({ user }) {
  return <Badge name={user.name} />;   // finally, someone who wants it
}

// ── COMPOSED: the badge is created where the user value already lives, then
//    passed in as an element. No intermediate component ever sees it. ───────
function ComposedLayout({ children, panel }) {
  renders.composedMid++;
  return <Frame title="Composed">{children}{panel}</Frame>;
}

function Frame({ 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>
  );
}

function Badge({ name }) {
  return (
    <span style={{ background: "#4f46e5", color: "white", padding: "3px 10px", borderRadius: 12, fontSize: 13 }}>
      {name}
    </span>
  );
}

export default function App() {
  const [user] = useState({ name: "Ada Lovelace" });
  const [tick, setTick] = useState(0);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 460 }}>
      <DrilledLayout user={user}>
        <p style={{ margin: 0, fontSize: 13, color: "#666" }}>
          user passed through 3 components that never read it
        </p>
      </DrilledLayout>

      {/* The element is built HERE, where user already is. */}
      <ComposedLayout panel={<Badge name={user.name} />}>
        <p style={{ margin: 0, fontSize: 13, color: "#666" }}>
          user never appears in ComposedLayout&apos;s props at all
        </p>
      </ComposedLayout>

      <button onClick={() => setTick((t) => t + 1)}>re-render the app ({tick})</button>

      <p style={{ color: "#666", fontSize: 13 }}>
        Both render the same badge. The composed version needs no <code>user</code>
        prop on any intermediate component, so renaming or reshaping the value
        touches one file instead of four — and it needed no Context to get there.
      </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 56 of 119 decoded in the React.js track. One more won't hurt.

Back to track