skip to content

After lifting selection state into a page component, the value and its onChange are threaded through four intermediate React components that never use them. How do you judge whether that is a real problem, and what would you change?

level: seniorimportance: should knowfreq 50%

answer

  1. drilling is a description, not a verdict
  2. check the placement before the plumbing
  3. children are created in the owner's scope
  4. pass elements, not data, through layouts
  5. context is transport, not ownership

basics

~20 s

Judge it by how many layers ignore the props and how often the contract changes; one or two hops are fine. The first fix is composition — let the owner build the elements and pass them as children or slot props, so the middle layers never see the data. Context comes after that.

solid answer

~50 s

First I check whether it is actually a problem: two hops of explicit, typed props are readable and traceable, and swapping them for indirection makes things worse. It becomes a problem when several layers carry props they ignore, because those components are now un-reusable, and every change to the state's shape edits every file in the chain. My first fix is not context — it is composition. The owner already has the value in scope, so it can create the leaf elements itself and pass them down as `children` or as slot props; the intermediates just render `{children}` and never learn the data exists. That removes the drilling without adding a subscription mechanism. I reach for context when the value is genuinely tree-wide and consumed at positions the owner cannot know — session, theme, locale — or when the consumer's position is decided by the consumer rather than the owner.

code

javascript · 21 lines
javascript
import { useState } from 'react';

// Layout knows nothing about selection: it renders whatever it is handed.
function Layout({ sidebar, main }) {
  return (
    <div className="layout">
      <aside>{sidebar}</aside>
      <section>{main}</section>
    </div>
  );
}

export function Page({ items }) {
  const [selectedId, setSelectedId] = useState(null);
  return (
    <Layout
      sidebar={<ItemList items={items} selectedId={selectedId} onSelect={setSelectedId} />}
      main={<ItemDetail item={items.find((i) => i.id === selectedId)} />}
    />
  );
}

go deeper

for a junior

Know what prop drilling means — a value passed through components that do not use it — and that context exists as one alternative. Being able to name the problem accurately is enough at this level.

for a middle

Explain the composition fix concretely: the owner creates the element, the intermediate renders it as children or a slot prop, and the data never enters the middle layers because the JSX was written where the state lives.

for a senior

Interrogate the premise first — how deep, how often does the shape change, does the state even belong that high — and then walk the ladder from colocation through composition to context, saying what each rung costs. Resisting an unnecessary abstraction is part of the answer.

for a principal

Set the policy: what qualifies a value for a context in this codebase, how you stop providers accumulating at the root, and how you keep component APIs shaped so ownership of a value can move later without a rewrite across every consumer.

## Step one: decide whether it is a problem at all "Prop drilling" is a description, not a verdict. Explicit props are the most traceable data flow React has: you can see where a value comes from by reading the JSX, types check every hop, and deleting a feature makes the unused prop obvious. Two levels of pass-through costs almost nothing. The cost becomes real on three signals: - **Depth and breadth.** Several layers, or many props at once, each carried by components with no interest in them. - **Churn.** Every change to the shape of the state edits every file in the chain, so unrelated components appear in the diff of a feature that does not touch them. - **Reuse damage.** The middle components can no longer be used anywhere the value does not exist, because their props demand it. If none of those bite, leaving the props alone is the correct engineering answer, and saying so is a stronger interview response than reflexively reaching for a global. ## Step two: check the placement before changing the plumbing Drilling is sometimes a symptom of over-lifting. If only the leaf reads and writes the value, the value should move down to the leaf and the whole chain disappears. Only when the value genuinely has consumers in different branches does it belong at the ancestor — and then the question is how it gets there, not whether it should be there. ## Step three: composition, before context The underused fix is inversion of the rendering. JSX children are created in the *scope where they are written*, so an element built by the owner already closes over the owner's state. The intermediate component receives it as an opaque node and renders it without knowing anything about it. ```jsx // Drilled: every layer must declare and forward selectedId + onSelect <Layout selectedId={id} onSelect={setId} /> // Composed: Layout only knows it has a sidebar slot <Layout sidebar={<ItemList selectedId={id} onSelect={setId} />} /> ``` Inside `Layout`, the code is `function Layout({ sidebar }) { return <aside>{sidebar}</aside>; }`. The selection state never enters `Layout`'s props, its types, or its tests. The same move works with `children` when the slot is the main content, and with several named element props when a layout has multiple regions. What you have done is separate *where a component renders* from *who provides its data*. That is the structural answer to drilling, and it costs nothing at runtime: no subscription, no provider, no new mechanism to reason about. ## Step four: when composition cannot reach Composition works when the owner knows *what* to render at the slot. It does not work when the consumer's position is unknown to the owner — a deeply nested, dynamically composed tree where any descendant may need the value, or a value read by dozens of unrelated components at arbitrary depths. That is the shape context exists for: session and current user, theme, locale, a form's registration object. Two qualifiers matter when you name context in an interview. First, context is a transport mechanism, not a state manager — something still has to own the state with `useState` or `useReducer` above the provider. Second, everything that reads a context re-renders when its value changes, so context suits values that are read widely and change rarely; putting a value that changes on every keystroke into a wide provider trades a drilling problem for a rendering one. ## Step five: when neither is enough If many distant components read overlapping slices of frequently changing state, and you want subscriptions at slice granularity rather than at provider granularity, that is the argument for an external store — a decision made on those grounds, not because "props got annoying". ## The ladder, in order 1. Colocate — can the state move down and delete the chain? 2. Keep the props — is the chain short and stable enough to be the clearest option? 3. Compose — can the owner build the element and pass it through as a slot? 4. Context — is the value tree-wide, widely read, and rarely changed? 5. Store — many distant readers, fine-grained slices, frequent updates? Moving down the ladder trades explicitness for reach. The mistake interviewers are probing for is jumping from step 1 to step 4 or 5 without ever considering steps 2 and 3. ## What interviewers listen for That you interrogate the premise before fixing it; that composition is on your list at all, since many candidates go straight from props to context; and that you can articulate what each rung costs rather than naming a favourite tool.

  • Why does passing elements as children remove the drilling instead of just moving it?
    Because the element is created in the owner's scope, where the state already is — the JSX closes over `selectedId` and `setSelectedId` at the point it is written. The intermediate component receives an opaque node it renders as-is, so the data never appears in its props, its types, or its tests. Nothing is forwarded; the value simply never enters the middle of the tree.
  • When is composition not an available fix?
    When the owner cannot know what belongs at the consuming position — a dynamically composed tree where any descendant might need the value, or a value read by many unrelated components at arbitrary depths. Then the consumer, not the owner, decides it needs the data, and a subscription mechanism such as context is the right shape.
  • Is prop drilling ever the best option for a long-lived codebase?
    Yes, when the chain is short and the contract is stable. Explicit props are the most traceable flow React offers: the source is visible in the JSX, types check every hop, and an unused prop is obvious. Replacing that with indirection to save two prop declarations makes the data flow harder to follow, not easier.
  • What would push you past context to an external store?
    Many distant components reading overlapping slices of state that changes frequently, where you want subscriptions at slice granularity — a context re-renders every consumer when its value changes, so it fits values that are read widely and change rarely. That is an argument about read patterns and update frequency, not about props being tedious to pass.

saying these in an interview costs you the question

  • Treats any prop passing through a layer as a defect to fix
  • Jumps straight from props to a global store
  • Never considers passing elements as children
  • Says context makes the app faster by avoiding prop passing
  • Thinks context owns state rather than transporting it

context