Advanced React Patterns
As your React app grows, you'll find yourself writing the same logic over and over â authentication checks, data fetching, scroll tracking, form validation. Patterns are reusable solutions that keep your code DRY (Don't Repeat Yourself) and your architecture clean.
This chapter covers the major patterns, when to use each one, and how they've evolved over time.
Higher-Order Components (HOCs)
A Higher-Order Component is a function that takes a component and returns a new, enhanced component. It's a wrapper that adds behavior without modifying the original.
Think of it like a phone case. The phone (your component) stays the same, but the case (HOC) adds protection (authentication), style (theming), or features (analytics tracking).
The Classic Example: withAuth
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user, loading } = useAuth();
if (loading) return <p>Loading...</p>;
if (!user) return <p>Please log in to view this page.</p>;
return <WrappedComponent {...props} user={user} />;
};
}
// Usage
function Dashboard({ user }) {
return <h1>Welcome back, {user.name}!</h1>;
}
const ProtectedDashboard = withAuth(Dashboard);
// In your app
<ProtectedDashboard />Now ProtectedDashboard automatically handles authentication. You didn't change Dashboard at all â you just wrapped it.
Another Example: withLogger
function withLogger(WrappedComponent) {
return function LoggedComponent(props) {
useEffect(() => {
console.log(`${WrappedComponent.name} mounted`);
return () => console.log(`${WrappedComponent.name} unmounted`);
}, []);
return <WrappedComponent {...props} />;
};
}
const LoggedDashboard = withLogger(Dashboard);HOC Drawbacks
- Wrapper hell â stacking multiple HOCs makes debugging painful:
withAuth(withLogger(withTheme(Dashboard))) - Prop collisions â if two HOCs inject a prop with the same name, one overwrites the other
- Hard to trace â React DevTools shows a nested tree of anonymous wrappers
Common Mistake: Using HOCs when a custom hook would be simpler. HOCs were the go-to pattern before hooks existed. Today, reach for a custom hook first and only use HOCs when you need to wrap the rendered JSX (like adding a loading screen or redirect).
Render Props
A render prop is a component that takes a function as a prop and calls it to determine what to render. Instead of the component deciding what the UI looks like, you decide â the component just provides the data or behavior.
Think of it like a food delivery service. The service (component) gets the food (data) ready, but you decide how to plate and serve it (what to render).
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return render(position);
}
// Usage â you decide what to render with the mouse position
function App() {
return (
<div>
<MouseTracker
render={({ x, y }) => (
<p>The mouse is at ({x}, {y})</p>
)}
/>
<MouseTracker
render={({ x, y }) => (
<div
style={{
position: 'fixed',
left: x + 10,
top: y + 10,
background: 'yellow',
padding: '4px 8px',
}}
>
Tooltip follows your cursor!
</div>
)}
/>
</div>
);
}Same MouseTracker behavior, completely different UIs. That's the power of render props â separation of behavior from presentation.
The "Children as a Function" Variant
Instead of a named prop, you can use children:
function DataFetcher({ url, children }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => { setData(data); setLoading(false); });
}, [url]);
return children({ data, loading });
}
// Usage
<DataFetcher url="/api/users">
{({ data, loading }) =>
loading ? <p>Loading...</p> : <UserList users={data} />
}
</DataFetcher>Remember: Render props are powerful but can make your JSX harder to read with deeply nested callbacks. In most cases today, a custom hook is a cleaner alternative.
Custom Hooks â The Modern Solution
Custom hooks are the modern replacement for both HOCs and render props. They let you extract and share stateful logic as a plain function.
Think of it like creating your own recipe card. Instead of explaining the same cooking steps every time, you write them down once and just say "follow the recipe" whenever you need it.
From Render Prop to Custom Hook
That MouseTracker render prop? Here's the custom hook version:
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return position;
}
// Usage â clean and simple
function CursorTooltip() {
const { x, y } = useMousePosition();
return (
<div style={{ position: 'fixed', left: x + 10, top: y + 10 }}>
Tooltip!
</div>
);
}
function CursorCoordinates() {
const { x, y } = useMousePosition();
return <p>Mouse: ({x}, {y})</p>;
}No wrappers, no render callbacks, no extra nesting. Just call the hook and use the data.
Real-World Example: useInfiniteScroll
Let's build a practical custom hook that detects when the user scrolls near the bottom of a page:
function useInfiniteScroll(callback, options = {}) {
const { threshold = 200 } = options;
const [isFetching, setIsFetching] = useState(false);
useEffect(() => {
function handleScroll() {
const scrolledToBottom =
window.innerHeight + window.scrollY >= document.body.offsetHeight - threshold;
if (scrolledToBottom && !isFetching) {
setIsFetching(true);
}
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [isFetching, threshold]);
useEffect(() => {
if (!isFetching) return;
callback().then(() => {
setIsFetching(false);
});
}, [isFetching, callback]);
return { isFetching };
}
// Usage
function UserFeed() {
const [users, setUsers] = useState([]);
const [page, setPage] = useState(1);
const loadMore = useCallback(async () => {
const newUsers = await fetch(`/api/users?page=${page}`).then(r => r.json());
setUsers(prev => [...prev, ...newUsers]);
setPage(prev => prev + 1);
}, [page]);
const { isFetching } = useInfiniteScroll(loadMore);
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
{isFetching && <p>Loading more users...</p>}
</div>
);
}Rules for Custom Hooks
- Name starts with
useâ this tells React (and the linter) it's a hook - Can call other hooks â
useState,useEffect, other custom hooks - Each call gets its own state â two components using
useMousePositioneach have independent state
Think of it like: Custom hooks are like USB adapters. They package up complex wiring (state, effects, refs) into a simple plug. Any component can plug in and get the behavior without knowing the internals.
Compound Components
Compound components are a group of components that work together and share implicit state. The parent manages the state, and the children tap into it via context.
Think of it like a TV and remote control. They're separate pieces, but they work together through a shared connection (infrared signal = context). You can arrange the remote's buttons however you want, but they all control the same TV.
const TabsContext = React.createContext(null);
function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
function Tab({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
const isActive = activeTab === value;
return (
<button
role="tab"
className={isActive ? 'tab active' : 'tab'}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
}
function TabPanel({ value, children }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== value) return null;
return <div role="tabpanel">{children}</div>;
}
Tabs.TabList = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;Usage â the consumer controls the layout and composition:
function SettingsPage() {
return (
<Tabs defaultTab="profile">
<Tabs.TabList>
<Tabs.Tab value="profile">Profile</Tabs.Tab>
<Tabs.Tab value="security">Security</Tabs.Tab>
<Tabs.Tab value="notifications">Notifications</Tabs.Tab>
</Tabs.TabList>
<Tabs.Panel value="profile">
<ProfileSettings />
</Tabs.Panel>
<Tabs.Panel value="security">
<SecuritySettings />
</Tabs.Panel>
<Tabs.Panel value="notifications">
<NotificationSettings />
</Tabs.Panel>
</Tabs>
);
}The consumer decides how many tabs, what order, what goes in each panel. The Tabs family handles all the state management behind the scenes.
When to use compound components:
- Component libraries (Select, Tabs, Accordion, Menu)
- When users need flexible composition with shared state
- When props drilling would create a messy API
Composition Over Inheritance
React has no class inheritance patterns. Instead, everything is built through composition â combining simple pieces to create complex UIs.
Think of it like LEGO blocks. You don't create a new type of brick by inheriting from an existing brick. You combine different bricks to build whatever you want.
The children Pattern
The simplest form of composition:
function Card({ children }) {
return <div className="card">{children}</div>;
}
function InfoCard({ title, children }) {
return (
<Card>
<h2>{title}</h2>
<div>{children}</div>
</Card>
);
}
// Usage
<InfoCard title="Welcome">
<p>This is a composed component built from simple pieces.</p>
</InfoCard>The Slot Pattern
For layouts with multiple "holes" to fill:
function PageLayout({ header, sidebar, children }) {
return (
<div className="layout">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
// Usage â each slot is filled independently
<PageLayout
header={<NavBar />}
sidebar={<SideMenu />}
>
<ArticleContent />
</PageLayout>Specialization
Creating specific components from general ones:
function Button({ variant, size, children, ...props }) {
return (
<button className={`btn btn-${variant} btn-${size}`} {...props}>
{children}
</button>
);
}
function DangerButton(props) {
return <Button variant="danger" {...props} />;
}
function SmallButton(props) {
return <Button size="small" {...props} />;
}DangerButton isn't a subclass of Button. It's a composition â a Button with a pre-filled prop.
Remember: If you ever think "I need to extend this component," stop and think about composition instead. Can you wrap it? Can you pass it as children? Can you pre-fill some props? Almost always, the answer is yes.
Pattern Selection Guide
Here's when to reach for each pattern:
| Pattern | Use When | Example |
|---|---|---|
| Custom Hook | You need to share stateful logic between components | useAuth, useInfiniteScroll, useLocalStorage |
| Compound Components | You need flexible composition with shared state | Tabs, Select, Accordion, Menu |
| Composition (children/slots) | You need layout flexibility without shared state | Card, PageLayout, Modal |
| HOC | You need to wrap many components with the same behavior that includes JSX | withAuth (shows login screen), withErrorBoundary |
| Render Props | You need inversion of control â the consumer decides what to render | Rare today â usually a custom hook is better |
The Decision Flowchart
- Do you need to share logic (no JSX)? Use a custom hook
- Do you need shared state with flexible child composition? Use compound components
- Do you need to wrap many components with conditional rendering? Use an HOC
- Do you need the consumer to control what renders with shared data? Use render props
- Do you just need flexible layout? Use composition (children/slots)
Interview Q&A:
Q: When would you choose a custom hook over a Higher-Order Component?
Almost always. Custom hooks are simpler, don't create extra wrapper elements in the component tree, don't have prop collision problems, and are easier to compose (you can call multiple hooks in one component). The only time an HOC might still be useful is when you need to wrap the component's rendered output â like showing a login screen instead of the component, or wrapping it in an error boundary. But even those cases can often be handled with composition.
Q: What's the main benefit of compound components over a single component with many props?
Flexibility. A <Tabs items={[...]} /> component locks consumers into a fixed structure. Compound components like <Tabs>, <Tab>, <TabPanel> let consumers control the order, layout, and content of each piece while still sharing state. It's the difference between a pre-built desk and LEGO blocks.
Chapter Summary
| Pattern | Key Idea | Analogy |
|---|---|---|
| HOC | Wrap a component with extra behavior | Phone case adds features to the phone |
| Render Props | Let the consumer decide what to render | Food delivery â you plate the food yourself |
| Custom Hook | Extract and share stateful logic | Recipe card â write it once, reuse everywhere |
| Compound Components | Components that work together via shared context | TV and remote â separate pieces, shared connection |
| Composition | Combine simple pieces into complex UIs | LEGO blocks â build anything from basic pieces |
The evolution of React patterns tells a clear story:
- 2015-2018: HOCs and render props were the main tools
- 2019+: Custom hooks replaced most HOC and render prop use cases
- Today: Custom hooks for logic, compound components for flexible UI, composition for everything else