Skip to the solution
MediumFrontend6 min read

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

553 views

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 drilling
  • context api
  • state management
  • performance

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 · 9 sections

Try it in the playground

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

ReactThe drilled version, then the same tree fixed by composition
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>
  );
}

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 55 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 interact with external APIs in React?

Continue