Welcome to the intricate world of ReactJS, where prop drilling often becomes a tricky puzzle to solve. You’re probably used to passing props down the component tree, but have you noticed how this gets messier as your app grows? In this article, I’m going to demonstrate this exact challenge. Forget about the basic what and why of React—let’s tackle the how to properly manage props in complex applications.
Ready to simplify your React life? Let’s dive in!
Problem?
Essentially, deep prop drilling is all about passing props through multiple component layers. Lets picture a scenario: you have a grandparent, parent, and child component. The top-level application holds data that the child needs, but to get there, it must travel through the grandparent and parent, even if the parent doesn’t need it.

This seemingly simple task can lead to several issues:
- Maintainability Concerns: As your application grows, tracking and managing these props through various layers becomes a Herculean task.
- Increased Complexity: With props weaving through multiple components, the relationship between them becomes convoluted, turning your code into a complex web that’s hard to untangle.
- Potential for Bugs and Decreased Readability: More props snaking through more components increase the chance for bugs. It also makes your code less readable, turning what should be a simple update into a debugging nightmare.
When we peel back the layers of our React applications, the repercussions of deep prop drilling are laid bare. It’s not just about the extra code; it’s the ripple effect on code quality and the daily life of a developer that deserves attention.
On code quality
Consider a feature as simple as adding a user’s preference. If this preference needs to reflect across multiple components, without deep prop drilling, the implementation is straightforward.
However, with deep prop drilling, you must thread this preference through various unrelated components, bloating each with unnecessary props. This bloat can obscure the intended purpose of components, leading to a codebase that’s harder to understand and modify.
On developer experience
For the person writing the code, this means more headaches. Every time you want to add or fix something, you have to follow a trail of breadcrumbs through your code to find where everything connects. It’s like untangling a knotted-up necklace — time-consuming and frustrating.
A real example
Let’s say you have a little switch in your app that changes the application theme. Simple, right? But with deep prop drilling, you need to send that switch’s “light” or “dark” state through every level of your app. As your app grows, this once-simple switch can become a big hassle, turning a quick update into a big project.
This is what I mean. The following App component holds the state for the theme and a method called toggleTheme to change it.
import React, { useState } from 'react';
import Grandparent from './Grandparent';
const App = () => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<div className={`app ${theme}`}>
<Grandparent theme={theme} toggleTheme={toggleTheme} />
</div>
);
};
export default App;The theme and toggleTheme are passed down through Grandparent and Parent components.
import React from 'react';
import Parent from './Parent';
const Grandparent = ({ theme, toggleTheme }) => {
return (
<div>
<Parent theme={theme} toggleTheme={toggleTheme} />
</div>
);
};
export default Grandparent;import React from 'react';
import Child from './Child';
const Parent = ({ theme, toggleTheme }) => {
return (
<div>
<Child theme={theme} toggleTheme={toggleTheme} />
</div>
);
};
export default Parent;And finally, the Child component contains a button that actually toggles the theme.
import React from 'react';
const Child = ({ theme, toggleTheme }) => {
return (
<div>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Theme
</button>
</div>
);
};See? This example clearly shows what deep prop drilling looks like: we’re passing the theme and toggleTheme all the way down to the Child component that actually needs to use them.
Honestly, I’m not a fan of this approach. Having worked with many React codebases, I find it frustrating to wade through such code. It feels like being in a maze, trying to trace back where everything comes from and where it’s supposed to go. But nonetheless, we sometimes have to deal with it, especially when working with older React codebases where this pattern is all too common.
This is the scenario we are aiming to refactor in later sections to avoid deep prop drilling.
Navigating away from deep prop drilling
In the React world, deep prop drilling is like navigating a maze. But no worries, we have smart ways to bypass this. We’re going to dive into two common techniques.
Using React Context
This is our first approach to avoid deep prop drilling. React Context acts like a messenger, delivering props directly to components, no matter their level in the tree. It’s a straightforward way to share data across different components without the hassle of passing props through each level.
React Context allows you to share values like state and functions across your component tree without having to pass props down manually at every level. To use React Context, you first create a context using createContext. Then, you wrap your component tree with a Context.Provider, which allows all child components to access the context’s value.
Here’s our refactored code:
import React, { useState, createContext, useContext } from "react";
// Create a Context for the theme
const ThemeContext = createContext({ theme: "light" });
// A component that provides the theme to its children
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
const Grandparent = () => (
<div>
<Parent />
</div>
);
const Parent = () => (
<div>
<Child />
</div>
);
// Use the Context in the Child component
const Child = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
Switch to {theme === "light" ? "Dark" : "Light"} Theme
</button>
);
};
// In the App component, wrap the part of our app where
// we need the theme in the ThemeProvider
const App = () => {
const { theme } = useContext(ThemeContext);
return (
<ThemeProvider>
<div className={`app ${theme}`}>
<Grandparent />
</div>
</ThemeProvider>
);
};
export default App;
In the above example, we create a ThemeContext and a ThemeProvider component that holds the theme state. The ThemeProvider wraps the entire component tree so that any component can access the theme state. The Child component uses useContext to retrieve and use the theme and toggleTheme from ThemeContext, allowing it to change the theme without prop drilling. Pretty simple eh?
Component composition
While React Context is a useful tool for certain scenarios, it’s not always the best solution for prop drilling. The more recommended approach is component composition. This method involves creating distinct components for specific functionalities, thereby reducing the need to pass props across many layers.
Instead of consuming the context directly in the Child, we create a separate ThemeToggle component. In component composition, instead of embedding all logic within a single component or passing props deeply, we break down our UI into smaller, reusable components. Each component takes care of its own functionality, leading to a cleaner and more modular structure.
This approach not only simplifies the component structure but also enhances reusability and maintainability. Alongside component composition, state management libraries can be used selectively when necessary to further streamline state handling in your React application.
Now, shall we?
import React, { useState } from "react";
const ThemeToggle = ({ theme, setTheme }) => {
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<button onClick={toggleTheme}>
Switch to {theme === "light" ? "Dark" : "Light"} Theme
</button>
);
};
const Grandparent = ({ children }) => (
<div>
{/*Grandparent specific code*/}
{children}
</div>
);
const Parent = ({ children }) => (
<div>
{/*Parent specific code*/}
{children}
</div>
);
// Use the Context in the Child component
const Child = ({ children }) => {
return (
<div>
{/*Child specific code*/}
{children}
</div>
);
};
const App = () => {
const [theme, setTheme] = useState("light");
return (
<div className={`app ${theme}`}>
<Grandparent>
<Parent>
<Child>
<ThemeToggle {...{ theme, setTheme }} />
</Child>
</Parent>
</Grandparent>
</div>
);
};
export default App;
See? Focus on the ThemeToggle component. It directly receives theme and setTheme, encapsulating the theme toggling functionality. This approach allows parent components (Grandparent, Parent, Child) to simply pass down their children, streamlining the component structure. The App component, acting as the state holder for theme, directly provides the necessary props only to ThemeToggle. This setup exemplifies the power of composition in creating a cleaner, more maintainable React architecture, avoiding the pitfalls of prop drilling.
Conclusion
In wrapping up, the main idea in avoiding prop drilling is to smartly pass props where needed. With our ThemeToggle component, we show how to provide necessary props directly, bypassing the need to drill through several component levels. This method simplifies our React code, making it cleaner and easier to maintain. In essence, using component composition in React helps us build more modular and understandable components, leading to more efficient and streamlined development.
Thanks for reading! 🥰