Hey there, Fellow Dev! đź‘‹
We need to talk about a disease that plagues many React codebases. It’s called Prop Drilling.
You know the symptoms. You need to pass a user object from your App component all the way down to a Avatar component nested 5 levels deep.
So you do this:
App -> Dashboard -> Header -> UserProfile -> Avatar.
Every single component in that chain has to accept user as a prop and pass it down, even if they don’t use it themselves. It’s tedious, it creates “Zombie Props”, and it makes refactoring a nightmare.
Many juniors immediately reach for Context API or Redux to solve this. But wait! There is a simpler, more “React” way to solve this.
It’s called Component Composition.
The “Drilling” Problem
Let’s look at the mess we are trying to fix.
const App = () => {
const [user, setUser] = useState({ name: "Alex" });
return <Dashboard user={user} />;
};
const Dashboard = ({ user }) => {
// Dashboard doesn't need user, but Header does!
return (
<div className="layout">
<Header user={user} />
<MainContent />
</div>
);
};
const Header = ({ user }) => {
// Header doesn't need user, but UserProfile does!
return (
<header>
<h1>My App</h1>
<UserProfile user={user} />
</header>
);
};
const UserProfile = ({ user }) => {
return <div>Welcome, {user.name}!</div>;
};
See Dashboard and Header? They are just “middlemen”. They are polluted with props they don’t care about.
The Solution: “Lift Content Up”
Instead of App telling Dashboard “Here is some data, please give it to your child”, App can simply say “Here is the CHILD itself”.
We use the special children prop.
const App = () => {
const [user, setUser] = useState({ name: "Alex" });
// We create the component HERE, where we have the data
return (
<Dashboard>
<Header>
<UserProfile user={user} />
</Header>
</Dashboard>
);
};
Wait, for this to work, we need to update our components to accept children.
const Dashboard = ({ children }) => {
return (
<div className="layout">
{children} {/* Just render whatever was passed! */}
<MainContent />
</div>
);
};
const Header = ({ children }) => {
return (
<header>
<h1>My App</h1>
{children} {/* Just render the profile here! */}
</header>
);
};
Why is this better?
- No Dead Props:
DashboardandHeaderno longer know aboutuser. You can change the user data structure without touching them. - Flexibility:
Appis now in full control. If you want to replaceUserProfilewithLoginButton, you just change it inApp. You don’t need to editDashboardorHeader. - No Context Overhead: You didn’t need to create a Context Provider, wrap your app, and use hooks. You just used standard props.
”Slots” Pattern (Named Composition)
Sometimes children isn’t enough because you need to place things in multiple spots (like a Sidebar and a Main Content area).
You can pass components as regular props!
const Layout = ({ left, right }) => {
return (
<div className="flex">
<div className="w-1/3">{left}</div>
<div className="w-2/3">{right}</div>
</div>
);
};
const App = () => {
return (
<Layout
left={<Sidebar />}
right={<NewsFeed />}
/>
);
};
This is exactly how you would pass a string or a number. In React, Elements are just values. You can pass them around just like variables.
Closing
Before you reach for Context or a global state management library, ask yourself: “Can I just lift the component up and pass it down?”
Component Composition is the backbone of React. Master it, and your components will be cleaner, more reusable, and easier to test.
Stop drilling, start composing!
Happy Coding! 🚀