Skip to main content

React Composition Patterns

Why Composition Over Inheritance

React discourages inheritance. Instead, compose behavior through:

  • Children pattern — wrap arbitrary content
  • Render props — pass rendering logic as a function
  • Slots — named props for specific positions

Inversion of control: the parent decides what renders, the child provides the structure.

Children Pattern

// Card accepts any content
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}

// Usage
<Card title="Profile">
<Avatar user={user} />
<Bio text={user.bio} />
</Card>

Slots Pattern (Named Children)

function Layout({ header, sidebar, main, footer }) {
return (
<div className="layout">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{main}</main>
<footer>{footer}</footer>
</div>
);
}

// Usage
<Layout
header={<NavBar />}
sidebar={<SideNav />}
main={<ArticleList />}
footer={<Footer />}
/>

Compound Components

Components that work together and share implicit state:

const TabContext = createContext();

function Tabs({ children, defaultTab }) {
const [active, setActive] = useState(defaultTab);
return (
<TabContext.Provider value={{ active, setActive }}>
<div className="tabs">{children}</div>
</TabContext.Provider>
);
}

function TabList({ children }) {
return <div role="tablist">{children}</div>;
}

function Tab({ id, children }) {
const { active, setActive } = useContext(TabContext);
return (
<button
role="tab"
aria-selected={active === id}
onClick={() => setActive(id)}
>
{children}
</button>
);
}

function TabPanel({ id, children }) {
const { active } = useContext(TabContext);
return active === id ? <div role="tabpanel">{children}</div> : null;
}

// Usage
<Tabs defaultTab="general">
<TabList>
<Tab id="general">General</Tab>
<Tab id="privacy">Privacy</Tab>
</TabList>
<TabPanel id="general"><GeneralSettings /></TabPanel>
<TabPanel id="privacy"><PrivacySettings /></TabPanel>
</Tabs>

Render Props Pattern

Pass a function as a prop that receives data and returns JSX:

function DataFetcher({ url, render }) {
const { data, loading, error } = useFetch(url);
return render({ data, loading, error });
}

// Usage: consumer controls what renders
<DataFetcher
url="/api/users"
render={({ data, loading }) =>
loading ? <Spinner /> : <UserList users={data} />
}
/>

// Children-as-function (same pattern)
<DataFetcher url="/api/users">
{({ data, loading }) => loading ? <Spinner /> : <UserList users={data} />}
</DataFetcher>

Provider Pattern

Wrap a tree to inject context without prop drilling:

function ThemeProvider({ theme, children }) {
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}

// Any depth component can consume
function Button() {
const theme = useContext(ThemeContext);
return <button className={`btn-${theme}`}>Click</button>;
}

When to Use Each Pattern

PatternWhenExample
ChildrenArbitrary nested contentCard, Modal, Layout
SlotsSpecific named regionsPage layout, Dialog
CompoundRelated components sharing stateTabs, Accordion, Form
Render PropsConsumer controls renderingData fetcher, Intersection Observer
ProviderCross-tree state without drillingTheme, Auth, Cart

Content from Frontend-Master-Prep-Series03-react