Search IOCombats

Search challenges, guides, questions and articles

What Is Prop Drilling and How to Avoid It

Intermediate10 min interview
Skills tested:
Identifying prop drilling and knowing at what depth it becomes a maintenance problemUsing component composition as the first and lightest solutionUsing Context API to make data available without prop drillingKnowing when a dedicated state manager (Zustand, Redux) is the right toolRecognizing that not all prop drilling is bad and that over-engineering is also a mistake

Advertisement

🧩 Scenario

Prop drilling is a common architecture problem in growing React codebases. It surfaces when a data source and its consumer are many component layers apart. The correct solution depends on how many consumers there are, how often the data changes, and whether the intermediate components have any stake in the data. A good engineer chooses the simplest solution that solves the problem.

Architecture Walkthrough

What Prop Drilling Is

Prop drilling occurs when data is passed through intermediate components that do not use it, only to reach a deeply nested consumer. Each intermediate component is coupled to the data: it must declare the prop in its interface and forward it explicitly. If the data shape changes or the consumer moves in the tree, every intermediary must be updated. Two or three levels of drilling is normal React; four or more levels usually signals a design problem.

The problem is not performance (React is fast at passing props), it is maintainability. Intermediate components become brittle, and the data's path through the tree is not obvious from any single file.

Solution 1: Component Composition

The most underused solution is component composition. Instead of passing data down through intermediaries, pass the component that needs the data as a children prop or as a named slot prop. The parent that owns the data renders the consumer directly and passes the data there, without threading it through intermediate components.

This is the solution React's own documentation recommends first. It requires no new API and no additional state management infrastructure.

Solution 2: Context API

When the same data needs to be available to many unrelated components scattered across the tree (user session, theme, locale, feature flags), Context is the right tool. The Provider wraps the subtree at the appropriate level and any descendant reads the value with useContext. No intermediate component needs to know about the data.

The tradeoff is that all consumers re-render when the context value changes. Split contexts by update frequency to mitigate this.

Solutions 3 and 4: State Managers and Custom Hooks

Zustand, Jotai, and Redux Toolkit provide stores that components subscribe to directly, with selector-based subscriptions that prevent unnecessary re-renders. They are appropriate when many components across the app share state that updates frequently. Custom hooks that use useContext or store APIs provide a clean API layer regardless of which solution is used underneath.


Key Code Explained

// The problem: prop drilling through three layers
function App() {
  const user = useCurrentUser();
  return <Layout user={user} />;
}
function Layout({ user }: { user: User }) {
  return <Sidebar user={user} />; // Layout doesn't use user
}
function Sidebar({ user }: { user: User }) {
  return <UserMenu user={user} />; // Sidebar doesn't use user
}
function UserMenu({ user }: { user: User }) {
  return <p>Hello, {user.name}</p>; // Only UserMenu uses user
}


// Solution 1: Component composition — pass the consumer, not the data
function App() {
  const user = useCurrentUser();
  // App renders UserMenu directly and passes user here, not through Layout/Sidebar
  return <Layout sidebar={<UserMenu user={user} />} />;
}
function Layout({ sidebar }: { sidebar: React.ReactNode }) {
  return (
    <div>
      <aside>{sidebar}</aside>
      <main>...</main>
    </div>
  );
}
function Sidebar() {
  // No user prop needed — just renders children
  return <div className="sidebar">{/* slots filled by parent */}</div>;
}
function UserMenu({ user }: { user: User }) {
  return <p>Hello, {user.name}</p>;
}
// Layout and Sidebar are no longer coupled to User at all


// Solution 2: Context API
const UserContext = createContext<User | null>(null);

function useUser() {
  const user = useContext(UserContext);
  if (!user) throw new Error('useUser must be inside UserProvider');
  return user;
}

function App() {
  const user = useCurrentUser();
  return (
    <UserContext.Provider value={user}>
      <Layout /> {/* no user prop */}
    </UserContext.Provider>
  );
}
function Layout() {
  return <Sidebar />; // no user prop
}
function Sidebar() {
  return <UserMenu />; // no user prop
}
function UserMenu() {
  const user = useUser(); // reads from context directly
  return <p>Hello, {user.name}</p>;
}


// Solution 3: Zustand (lightweight state manager)
import { create } from 'zustand';

interface UserStore {
  user: User | null;
  setUser: (user: User) => void;
}

const useUserStore = create<UserStore>((set) => ({
  user: null,
  setUser: (user) => set({ user }),
}));

function UserMenu() {
  const user = useUserStore((state) => state.user); // fine-grained subscription
  return <p>Hello, {user?.name}</p>;
}
// Only re-renders when user changes, not when other store values change

The component composition solution is the most important one to demonstrate in an interview because it shows you understand React's core model before reaching for Context. Many prop drilling problems can be solved by passing JSX or components down instead of data, which requires no additional infrastructure.


Tradeoffs

SolutionComplexityRe-render costWhen to use
Prop drilling (1-2 deep)NoneNoneDefault React for shallow trees
CompositionLowNoneWhen consumer can be rendered by the data owner
ContextLow-mediumAll consumersShared data for many components at various depths
Zustand / ReduxMediumOnly subscribersFrequently updated state shared app-wide

What Interviewers Actually Check

  • Whether you can identify prop drilling and explain the maintainability problem
  • Whether you know component composition as the first solution to try
  • Whether you know Context and its re-render cost
  • Whether you know state managers as the appropriate tool for high-frequency, app-wide state
  • Whether you can articulate a decision framework rather than defaulting to one solution

Follow-Up Questions

  1. How does the children prop enable component composition as a prop drilling solution?
  2. If Context re-renders all consumers on value change, how does use-context-selector solve this, and how does it work internally?
  3. When would you prefer useReducer + Context over useState + Context for sharing actions alongside data?
  4. How does React Query or SWR change the prop drilling story for server data specifically?
  5. What is the compound component pattern and how does it use Context internally to avoid prop drilling within a component family?

Common Candidate Mistakes

  • Reaching for Context as the solution to any prop passing beyond one level, before considering composition
  • Not knowing that Context has a re-render cost and treating it as a zero-cost substitute for props
  • Installing a state management library (Redux) for data only three components need
  • Prop drilling both data and callbacks when both could be moved to the same Context
  • Not articulating why prop drilling becomes a problem only at three or more levels of intermediate components

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you identify a prop drilling problem and articulate why it becomes a maintenance issue?
  • Can you refactor a prop-drilled tree using component composition?
  • Can you lift data to Context and consume it with a custom hook without prop drilling?
  • Can you describe the re-render cost of Context and when it matters?
  • Can you articulate a decision framework: composition first, Context second, state manager third?

Summary

Prop drilling is the pattern of passing props through intermediate components that do not use them, only to reach a deeply nested consumer. One or two levels of prop passing is idiomatic React; three or more levels creates coupling between intermediate components and the data, making refactors painful and the data flow opaque.

The first solution to reach for is component composition: instead of threading data through intermediaries, the component that owns the data renders the consumer and passes data there directly. No intermediate component needs to know about the data. This requires no new infrastructure and is often the simplest fix.

Context is the right tool when the same data must be available to many unrelated consumers across the tree (user session, theme, locale). Any descendant can read the value with useContext without any intermediate component needing to forward it. The tradeoff is that all consumers re-render when the context value changes. For frequently updated, app-wide state with many consumers, a dedicated state manager like Zustand provides selector-based subscriptions that prevent unnecessary re-renders.

Frequently Asked Questions

Is prop drilling always bad?

No. Passing props one or two levels deep is idiomatic React. Prop drilling becomes a problem when props must pass through three or more intermediate components that do not use them, creating coupling and making refactors painful.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement