Prop drilling occurs when you pass the same prop through multiple intermediate components that do not directly use it. This makes components tightly coupled, harder to refactor, and difficult to reuse. React provides several solutions including Context API, state management libraries like Redux or Zustand, and component composition patterns that eliminate the need for drilling props through unnecessary layers.
Prop drilling is a common React pattern where you pass props down through multiple levels of components just to reach a component that actually uses them. This happens when data at a high level in your component tree needs to be accessed by a deeply nested component. While prop drilling works fine for small applications, it becomes problematic in larger codebases because it creates invisible dependencies between components that do not directly use the props. When you need to refactor or move components around, prop drilling makes this difficult because components become coupled to their parent structure. Additionally, it becomes hard to trace where props are coming from and what components actually depend on them. React provides several built-in solutions and design patterns to solve this problem without introducing unnecessary complexity.
First, audit your component tree to find where prop drilling is happening. Look for props that are passed to a component but not used by that component—instead, they are immediately passed to child components. Use your IDE's "Find References" feature to trace a prop through the component tree. Make a list of: which prop is being drilled, how many layers deep it goes, and which component actually uses it.
For data that is accessed by many components at different nesting levels (like theme, language, authentication status, or UI settings), create a Context. This allows any descendant component to access the value without drilling props.
// Create the context
const ThemeContext = React.createContext<Theme | undefined>(undefined);
// Create a provider component
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = React.useState<Theme>("light");
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
// Use the context anywhere without prop drilling
export function useTheme() {
const context = React.useContext(ThemeContext);
if (!context) throw new Error("useTheme must be used within ThemeProvider");
return context;
}
// In deeply nested components
function DeepComponent() {
const theme = useTheme(); // No props needed
return <div style={{ background: theme === "dark" ? "#000" : "#fff" }} />;
}Rather than managing state at the root and drilling it down, move state into the closest component that needs it. This makes components more reusable and self-contained. If multiple siblings or cousins need the same state, move it to their shared parent, not to the root.
// BEFORE: State at root, drilled through many layers
function App() {
const [isOpen, setIsOpen] = useState(false);
return <Level1 isOpen={isOpen} setIsOpen={setIsOpen} />;
}
function Level1({ isOpen, setIsOpen }) {
return <Level2 isOpen={isOpen} setIsOpen={setIsOpen} />;
}
function Level2({ isOpen, setIsOpen }) {
return <Modal isOpen={isOpen} setIsOpen={setIsOpen} />;
}
// AFTER: State moved to where it is used
function App() {
return <ModalSection />;
}
function ModalSection() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open</button>
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)} />
</>
);
}Instead of passing data as props, pass components as children. This avoids needing intermediate components to accept and forward props.
// BEFORE: Data drilled through intermediate component
function Page({ userData }) {
return <UserSection userData={userData} />;
}
function UserSection({ userData }) {
return <UserCard userData={userData} />;
}
function UserCard({ userData }) {
return <div>{userData.name}</div>;
}
// AFTER: Composition pattern, no drilling needed
function Page({ userData }) {
return (
<UserSection>
<UserCard name={userData.name} />
</UserSection>
);
}
function UserSection({ children }) {
return <div className="section">{children}</div>;
}For larger applications with complex state logic (like user data, permissions, shopping cart, or application settings), use a state management library. Popular options include Redux, Zustand, and Recoil. These libraries allow any component to access or modify global state without prop drilling.
// Using Zustand (lightweight and simple)
import { create } from "zustand";
const useAuthStore = create((set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null }),
}));
// Use in any component without drilling
function Profile() {
const { user, logout } = useAuthStore();
return (
<div>
{user?.name}
<button onClick={logout}>Logout</button>
</div>
);
}For cross-cutting concerns or props that need to be injected into components, use HOCs or custom hooks. This keeps intermediate components clean without needing to accept and forward props.
// HOC pattern
function withTheme(Component) {
return function ThemedComponent(props) {
const theme = useTheme();
return <Component {...props} theme={theme} />;
};
}
const ThemedButton = withTheme(Button);
// Custom Hook pattern (preferred in modern React)
function useThemeProps() {
return { theme: useTheme() };
}
function Button() {
const { theme } = useThemeProps();
return <button style={{ color: theme.color }} />;
}Sometimes prop drilling exists because components are broken into too many small pieces unnecessarily. Review your component structure and consider combining components that are tightly related. Keep the render logic co-located with the state it uses.
// BEFORE: Too many component layers
function App() {
const [count, setCount] = useState(0);
return <Container count={count} setCount={setCount} />;
}
function Container({ count, setCount }) {
return <Content count={count} setCount={setCount} />;
}
function Content({ count, setCount }) {
return <Counter count={count} setCount={setCount} />;
}
// AFTER: Combine when the hierarchy is unnecessary
function App() {
const [count, setCount] = useState(0);
return (
<Container>
<Counter count={count} onIncrement={() => setCount(count + 1)} />
</Container>
);
}After refactoring to eliminate prop drilling, test that components still work when used in different contexts. The goal is to make components more reusable. Write unit tests that render components with minimal props. Verify that Context consumers work correctly by wrapping them in test providers. Use React DevTools Profiler to ensure you did not introduce unnecessary re-renders.
Prop drilling is often a symptom of a deeper architectural issue. Before jumping to Context or Redux, carefully consider your component structure. Sometimes the best solution is to restructure components so that state is closer to where it is used. Context API is perfect for truly global state (theme, language, authentication), but overusing it can make components depend on implicit context that is hard to test and debug. When choosing between Context API and a state management library, consider: Context API is free and built-in (good for small to medium apps), while Redux/Zustand add complexity but provide better DevTools, time-travel debugging, and middleware. For teams, establish a convention about when to use each approach. Consider that some "prop drilling" is actually acceptable—passing props through 1-2 layers is fine and keeps dependencies explicit. The goal is not to eliminate all props, but to eliminate unnecessary drilling that makes components harder to understand and maintain. React's render props and children patterns are often underutilized—they are powerful tools for composition that can reduce drilling without introducing state management libraries.
React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
useNavigate() may be used only in the context of a <Router> component.
useNavigate() may be used only in the context of a Router component
Cannot find module or its corresponding type declarations
How to fix "Cannot find module or type declarations" in Vite