title: Compound Components and Inversion of Control: Building Flexible, Low-Maintenance UI Kits date: 2026-07-08T07:31:40.012Z description: Blog about Compound Components and Inversion of Control: Building Flexible, Low-Maintenance UI Kits
Hey everyone, I’m Prashuk. I spend most of my days deep in the MERN stack, trying to figure out how to build UIs that don't break the moment a designer changes their mind.
If you’ve been building React apps for a while, you’ve definitely faced the "Mega-Component" nightmare. You build a simple Modal, and two weeks later, it has 25 props because every page needs a slightly different version.
Today, I want to talk about how I stopped writing those messy components and started using Compound Components and Inversion of Control (IoC).
The "Prop-Drilling" Trap
I ran into this issue when I was building a custom Dashboard for a client. We had a Table component. Initially, it just showed data. Then the client wanted a search bar. Then a filter. Then a "Download PDF" button that only appeared on some pages.
My component looked like this:
<Table data={data} showSearch={true} showFilter={false} onDownload={handleDownload} ... />
It was a mess to maintain. Every time I wanted to change the UI layout, I had to go into the base component and move <div>s around, which usually broke three other pages. Honestly, it was a headache.
What are Compound Components?
The idea is simple: instead of one giant component trying to do everything, you break it into smaller pieces that share a common state. Think of the native HTML <select> and <option> tags. You don’t pass options as an array prop to the select; you nest them.
In React, we use the Context API to make this happen. What worked for me was creating a parent component that holds the "brain" (the state) and sub-components that handle the "beauty" (the UI).
Inversion of Control (IoC)
This is a fancy term that basically means: Give the power back to the person using the component.
Instead of me (the component author) deciding that the "Close" button always sits at the top right, I let the developer using the component decide where to put it. By exporting sub-components like Modal.Header, Modal.CloseButton, and Modal.Body, I’m no longer the bottleneck for UI changes.
A Practical Example: The Tabs Component
In one of my projects, I had to build a tabbed interface. Usually, people pass an array of objects for tabs. But what if one tab needs a notification badge and another doesn't?
Here’s how I structured it using the Compound Component pattern:
import React, { useState, createContext, useContext } from 'react';
const TabContext = createContext();
// The Parent
export function Tabs({ children, defaultValue }) {
const [activeTab, setActiveTab] = useState(defaultValue);
return (
<TabContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs-container">{children}</div>
</TabContext.Provider>
);
}
// Sub-component: The Trigger
Tabs.Trigger = function Trigger({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabContext);
return (
<button
className={`px-4 py-2 ${activeTab === value ? 'border-b-2 border-blue-500' : ''}`}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
};
// Sub-component: The Content
Tabs.Content = function Content({ value, children }) {
const { activeTab } = useContext(TabContext);
return activeTab === value ? <div className="p-4">{children}</div> : null;
};
How you actually use it:
<Tabs defaultValue="profile">
<div className="flex space-x-4 border-b">
<Tabs.Trigger value="profile">Profile</Tabs.Trigger>
<Tabs.Trigger value="settings">Settings ⚙️</Tabs.Trigger>
</div>
<Tabs.Content value="profile">Welcome back, Prashuk!</Tabs.Content>
<Tabs.Content value="settings">Manage your account here.</Tabs.Content>
</Tabs>
Why this is better for Performance & UI
- Clean Code: Look at that usage. It’s readable. You can see the structure of the UI just by glancing at the code.
- No More Prop-Drilling: I'm not passing "activeTab" down manually through five layers of divs.
- Flexibility: If I want to move the Tab triggers to the bottom? I just move the code. I don't have to rewrite the
Tabscomponent logic. - Performance: Since sub-components only consume the context they need, it’s easier to optimize with
React.memoif things get heavy.
My Best Practices
- Don't over-engineer: If a component is truly simple (like a basic button), don't make it a compound component. Use this for things like Modals, Selects, Tabs, and Steppers.
- Use descriptive names: Stick to the
Parent.Childnaming convention. It makes it obvious thatTabs.Triggerbelongs toTabs. - Provide sensible defaults: Even with IoC, try to make the "default" look good so the next dev doesn't have to style everything from scratch.
Conclusion
Switching to Compound Components changed how I think about UI kits. It takes a bit more time to set up the Context and sub-components initially, but it saves hours of refactoring later.
The biggest learning for me? Stop trying to predict every feature request. Build a flexible system, give control to the consumer, and keep your components lean.
Give it a shot in your next project. It’ll make your code (and your life) a lot cleaner.
Catch you in the next one, Prashuk