Compound Components
Compound Components
Implicit State Sharing via Context
Compound components allow a parent component to implicitly share state with its children through React Context, eliminating the need for prop drilling. The parent owns the state and exposes it to children without requiring the consumer to wire up props manually.
import { createContext, useContext, useState, useCallback } from 'react';
const TabsContext = createContext(null);
function Tabs({ defaultIndex = 0, children, onChange }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
const selectTab = useCallback((index) => {
setActiveIndex(index);
onChange?.(index);
}, [onChange]);
return (
<TabsContext.Provider value={{ activeIndex, selectTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
function Tab({ index, children }) {
const { activeIndex, selectTab } = useContext(TabsContext);
return (
<button
role="tab"
aria-selected={activeIndex === index}
onClick={() => selectTab(index)}
className={activeIndex === index ? 'tab active' : 'tab'}
>
{children}
</button>
);
}
function TabPanel({ index, children }) {
const { activeIndex } = useContext(TabsContext);
if (activeIndex !== index) return null;
return <div role="tabpanel">{children}</div>;
}
// Usage — clean, declarative API
<Tabs onChange={(i) => console.log('Tab', i)}>
<TabList>
<Tab index={0}>Profile</Tab>
<Tab index={1}>Settings</Tab>
<Tab index={2}>Notifications</Tab>
</TabList>
<TabPanel index={0}>Profile content</TabPanel>
<TabPanel index={1}>Settings content</TabPanel>
<TabPanel index={2}>Notifications content</TabPanel>
</Tabs>
Why Compound Components?
The parent controls the shared state so consumers do not need to manage or pass it down. Adding new tabs requires zero changes to the parent — you just compose new Tab and TabPanel children. This pattern produces clean, declarative APIs that are easy to extend without breaking existing usage.
// Accordion compound component
const AccordionContext = createContext(null);
function Accordion({ allowMultiple = false, children }) {
const [openItems, setOpenItems] = useState(new Set());
const toggle = useCallback((id) => {
setOpenItems(prev => {
const next = new Set(allowMultiple ? prev : []);
if (prev.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, [allowMultiple]);
return (
<AccordionContext.Provider value={{ openItems, toggle }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
function AccordionItem({ id, children }) {
return <div className="accordion-item">{children}</div>;
}
function AccordionHeader({ id, children }) {
const { openItems, toggle } = useContext(AccordionContext);
return (
<button
onClick={() => toggle(id)}
aria-expanded={openItems.has(id)}
>
{children}
</button>
);
}
function AccordionPanel({ id, children }) {
const { openItems } = useContext(AccordionContext);
if (!openItems.has(id)) return null;
return <div className="accordion-panel">{children}</div>;
}
Render Props and HOCs
Render Props and HOCs
Render Props Pattern
A render prop is a function prop that a component calls instead of rendering its own markup. The parent supplies the rendering logic, giving full control over what and how content is rendered while the component manages the stateful logic.
import { useState, useEffect } from 'react';
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return position;
}
// Render prop component
function MouseTracker({ render }) {
const { x, y } = useMousePosition();
return render({ x, y });
}
// Usage
<MouseTracker
render={({ x, y }) => (
<div>
<p>Mouse is at ({x}, {y})</p>
<div
style={{
width: 20,
height: 20,
borderRadius: '50%',
background: 'red',
position: 'fixed',
left: x - 10,
top: y - 10,
}}
/>
</div>
)}
/>
Higher-Order Components (HOCs)
A HOC is a function that takes a component and returns a new component with enhanced behavior. HOCs wrap a component to inject props, add state, or modify rendering logic. They were the original pattern for code reuse before hooks existed.
function withLoading(WrappedComponent, LoadingComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) return <LoadingComponent />;
return <WrappedComponent {...props} />;
};
}
function UserProfile({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
const UserProfileWithLoading = withLoading(UserProfile, Spinner);
// Usage
<UserProfileWithLoading isLoading={loading} user={user} />
Combining Patterns
HOCs and render props serve similar purposes — they let you reuse stateful logic across components. Render props offer more composability (you can nest multiple render prop components), while HOCs provide cleaner usage syntax at the component level. Modern React favors custom hooks for most cases, but these patterns remain useful when you need to wrap a component tree or inject props into class components.
// HOC that adds authentication context
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
if (!user) return <Navigate to="/login" />;
return <WrappedComponent {...props} user={user} />;
};
}
// Render prop for toggling visibility
function Toggle({ initial = false, children }) {
const [on, setOn] = useState(initial);
const toggle = () => setOn(prev => !prev);
return children({ on, toggle });
}
// Combining render props
<Toggle initial={true}>
{({ on, toggle }) => (
<div>
<button onClick={toggle}>{on ? 'Hide' : 'Show'}</button>
{on && <DropdownMenu items={items} />}
</div>
)}
</Toggle>
Container and Presentational Components
Container and Presentational Components
Separating Data from UI
The container/presentational pattern splits components into two categories: containers handle data fetching, state management, and business logic; presentational components receive data via props and focus purely on rendering UI. This separation makes components easier to test, reuse, and reason about.
// Presentational component — receives data, renders UI
function UserListUI({ users, loading, error, onRetry }) {
if (loading) return <div className="skeleton-list">Loading...</div>;
if (error) {
return (
<div className="error-state">
<p>Failed to load users: {error}</p>
<button onClick={onRetry}>Retry</button>
</div>
);
}
return (
<ul className="user-list">
{users.map(user => (
<li key={user.id} className="user-item">
<img src={user.avatar} alt={user.name} />
<div>
<strong>{user.name}</strong>
<span>{user.email}</span>
</div>
</li>
))}
</ul>
);
}
// Container component — handles logic, delegates rendering
function UserListContainer() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchUsers = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { fetchUsers(); }, [fetchUsers]);
return (
<UserListUI
users={users}
loading={loading}
error={error}
onRetry={fetchUsers}
/>
);
}
When to Use This Pattern
Use the container/presentational split when a component does too many things — fetching data, managing state, and rendering complex UI. Extract the data logic into a container and keep presentational components pure functions of their props. This makes presentational components trivially testable (just pass props, assert output) and reusable in different contexts. With custom hooks, you can extract the container logic into a hook and keep the component simple.
// Extracting container logic into a custom hook
function useUsers() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchUsers = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('Failed to fetch');
setUsers(await res.json());
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { fetchUsers(); }, [fetchUsers]);
return { users, loading, error, retry: fetchUsers };
}
// Now the component is simple — hook does the heavy lifting
function UserList() {
const { users, loading, error, retry } = useUsers();
return <UserListUI users={users} loading={loading} error={error} onRetry={retry} />;
}
Custom Hooks for Shared Logic
Custom Hooks for Shared Logic
Extracting Reusable Stateful Logic
Custom hooks are the modern replacement for HOCs and render props. They let you extract component logic into reusable functions that follow the rules of hooks. A custom hook can use state, effects, context, and other hooks internally while exposing a clean API to consumers.
import { useState, useEffect, useCallback } from 'react';
// useToggle — simple state machine
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
// Usage
function Modal() {
const { value: isOpen, toggle, setFalse: close } = useToggle();
return (
<>
<button onClick={toggle}>Open Modal</button>
{isOpen && (
<div className="modal-overlay">
<div className="modal">
<h2>Modal Title</h2>
<p>Modal content goes here</p>
<button onClick={close}>Close</button>
</div>
</div>
)}
</>
);
}
useMediaQuery — Reactive Breakpoints
function useMediaQuery(query) {
const [matches, setMatches] = useState(
() => typeof window !== 'undefined' && window.matchMedia(query).matches
);
useEffect(() => {
const mq = window.matchMedia(query);
const handler = (e) => setMatches(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, [query]);
return matches;
}
// Usage
function ResponsiveLayout({ mobile, desktop }) {
const isDesktop = useMediaQuery('(min-width: 768px)');
return isDesktop ? desktop : mobile;
}
useLocalStorage — Persistent State
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value) => {
setStoredValue(prev => {
const valueToStore = value instanceof Function ? value(prev) : value;
window.localStorage.setItem(key, JSON.stringify(valueToStore));
return valueToStore;
});
}, [key]);
return [storedValue, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
);
}
useAnimation — Spring-based Animations
function useAnimation(duration = 300) {
const [progress, setProgress] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
const animate = useCallback(() => {
setIsAnimating(true);
setProgress(0);
const start = performance.now();
const frame = (now) => {
const elapsed = now - start;
const t = Math.min(elapsed / duration, 1);
setProgress(t);
if (t < 1) {
requestAnimationFrame(frame);
} else {
setIsAnimating(false);
}
};
requestAnimationFrame(frame);
}, [duration]);
return { progress, isAnimating, animate };
}
// Usage
function AnimatedBar() {
const { progress, animate } = useAnimation(500);
return (
<div>
<button onClick={animate}>Animate</button>
<div
style={{
width: `${progress * 100}%`,
height: 20,
background: 'blue',
transition: 'none',
}}
/>
</div>
);
}
Quiz
1. What problem do compound components solve compared to passing many props to a single component?
2. How do render props and higher-order components differ in how they share logic?
3. Why is the container/presentational pattern beneficial for testing?
Flashcards
Question
What are compound components and when should you use them?
Click to reveal answer
Answer
Compound components share implicit state via React Context so a parent controls shared behavior while children compose freely. Use them when building UI primitives where multiple related components need to coordinate state (tabs, accordions, dropdowns, menus). They produce clean declarative APIs and eliminate prop drilling within a component tree.
Question
Explain the render prop pattern with an example.
Click to reveal answer
Answer
A render prop is a function prop that a component calls with its internal state, letting the consumer control rendering. Example: `<MouseTracker render={({x, y}) => <p>Mouse at {x}, {y}</p>} />`. The component owns the state logic (tracking mouse position) and the consumer decides how to render it. This separates concerns and allows reuse without inheritance.
Question
How do custom hooks replace HOCs and render props?
Click to reveal answer
Answer
Custom hooks extract stateful logic into reusable functions that follow the rules of hooks. Unlike HOCs (which wrap components and inject props) or render props (which nest function children), hooks compose naturally at the top of a component. They avoid wrapper hell, have clearer naming, and work with any component. Example: `const { data, loading } = useFetch(url)` replaces a `withFetch` HOC entirely.
Revision Notes
Key Takeaways
- 1. Compound components use Context to share implicit state between parent and children, producing declarative APIs that are easy to extend without breaking existing usage
- 2. Render props pass a function that receives component state — the consumer controls rendering while the component manages logic; HOCs wrap components and inject enhanced props
- 3. Container/presentational split separates data fetching and state management (container) from UI rendering (presentational), making components easier to test and reuse
- 4. Custom hooks are the modern replacement for HOCs and render props — they extract stateful logic into composable functions without wrapper components
- 5. useToggle, useLocalStorage, useMediaQuery, and useAnimation are practical custom hook patterns that encapsulate common UI behaviors
- 6. HOCs remain useful for cross-cutting concerns like authentication and error boundaries where you need to wrap a component tree
- 7. When building compound components, always expose clean prop APIs and use Context only for implicit shared state, not for passing everything
Interview Tips
- • Build a Tabs compound component from scratch using Context — explain how implicit state sharing eliminates prop drilling
- • Compare render props vs HOCs vs custom hooks with concrete examples of when each is the right choice
- • Explain the container/presentational pattern and demonstrate how to refactor a bloated component into two focused pieces
- • Describe how you would build a useFetch hook that handles loading, error, abort, and cleanup — walk through the implementation
- • Discuss when HOCs are still preferred over hooks (error boundaries, class component wrappers, cross-cutting concerns)
Cheat Sheet
Compound Components
Parent owns state via Context; children read shared state implicitly — clean declarative API without prop drilling
Render Props
Component calls props.render(state) or props.children(state) — consumer controls rendering, component owns logic
HOC
const Enhanced = withHOC(WrappedComponent) — injects props, adds behavior, wraps component tree
Container
Handles data fetching, state, business logic — delegates rendering to presentational child
Presentational
Pure function of props — renders UI, no side effects, trivially testable
Custom Hooks
Function starting with use that encapsulates reusable stateful logic — modern replacement for HOCs and render props
useToggle
Simple boolean state with toggle/setTrue/setFalse — replaces manual useState+toggle pattern
useLocalStorage
useState that persists to localStorage — lazy initialization, JSON serialization, cross-tab sync
useMediaQuery
Reactive window.matchMedia — returns boolean for responsive breakpoints, updates on resize