Chapter 35 โ Advanced Component Patterns
๐ Definition
Advanced component patterns are reusable ways to structure React components so they are flexible (the caller controls layout and behaviour) without becoming a mess of props. The common ones are compound components, render props, higher-order components, controlled/uncontrolled props, the state-reducer pattern, portals, and error boundaries.
๐ป Pattern 1 โ Compound Components
Several components share implicit state through context, giving the caller full control of markup.
import { createContext, useContext, useState } from "react";
const TabsCtx = createContext(null);
export function Tabs({ defaultValue, children }) {
const [active, setActive] = useState(defaultValue);
return <TabsCtx.Provider value={{ active, setActive }}>{children}</TabsCtx.Provider>;
}
Tabs.List = function TabsList({ children }) {
return <div role="tablist">{children}</div>;
};
Tabs.Tab = function Tab({ value, children }) {
const { active, setActive } = useContext(TabsCtx);
return (
<button role="tab" aria-selected={active === value} onClick={() => setActive(value)}>
{children}
</button>
);
};
Tabs.Panel = function TabPanel({ value, children }) {
const { active } = useContext(TabsCtx);
return active === value ? <div role="tabpanel">{children}</div> : null;
};
// Usage โ the caller decides structure and order
<Tabs defaultValue="profile">
<Tabs.List>
<Tabs.Tab value="profile">Profile</Tabs.Tab>
<Tabs.Tab value="billing">Billing</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="profile"><Profile /></Tabs.Panel>
<Tabs.Panel value="billing"><Billing /></Tabs.Panel>
</Tabs>;Used by Radix UI, Headless UI, Reach UI and shadcn/ui.
๐ป Pattern 2 โ Render Props
A component receives a function and calls it to decide what to render.
function MouseTracker({ children }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
return (
<div onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })} style={{ height: 300 }}>
{children(pos)}
</div>
);
}
<MouseTracker>{({ x, y }) => <p>Mouse at {x}, {y}</p>}</MouseTracker>;Today a custom hook (useMousePosition()) usually replaces render props, but the pattern still appears in libraries (Formik's <Field>, React Router's older APIs, virtualised lists' rowRenderer).
๐ป Pattern 3 โ Higher-Order Component (HOC)
A function that takes a component and returns an enhanced component.
function withAuth(Component, { role } = {}) {
return function Protected(props) {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
if (!user) return <Navigate to="/login" replace />;
if (role && user.role !== role) return <p>Forbidden</p>;
return <Component {...props} user={user} />;
};
}
export default withAuth(AdminPage, { role: "admin" });Downsides: wrapper nesting ("wrapper hell"), prop-name collisions, harder typing. Prefer hooks for logic reuse and keep HOCs for cross-cutting wrapping.
๐ป Pattern 4 โ Controlled and Uncontrolled (Like <input>)
function useControllableState({ value, defaultValue, onChange }) {
const [internal, setInternal] = useState(defaultValue);
const isControlled = value !== undefined;
const current = isControlled ? value : internal;
const setValue = (next) => {
if (!isControlled) setInternal(next);
onChange?.(next);
};
return [current, setValue];
}
function Toggle({ on, defaultOn = false, onChange }) {
const [isOn, setIsOn] = useControllableState({ value: on, defaultValue: defaultOn, onChange });
return <button aria-pressed={isOn} onClick={() => setIsOn(!isOn)}>{isOn ? "ON" : "OFF"}</button>;
}
<Toggle defaultOn />; // uncontrolled
<Toggle on={enabled} onChange={setEnabled} />; // controlled by parent๐ป Pattern 5 โ State Reducer (Inversion of Control)
Let the caller override how state changes, without forking the component.
function counterReducer(state, action) {
switch (action.type) {
case "inc": return { count: state.count + 1 };
case "dec": return { count: state.count - 1 };
default: return state;
}
}
function useCounter({ reducer = counterReducer } = {}) {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return { ...state, inc: () => dispatch({ type: "inc" }), dec: () => dispatch({ type: "dec" }) };
}
// Caller: never go below zero
const { count, inc, dec } = useCounter({
reducer: (state, action) => {
const next = counterReducer(state, action);
return next.count < 0 ? state : next;
},
});๐ป Pattern 6 โ Portals (Modals, Tooltips, Toasts)
Render children into a DOM node outside the parent hierarchy, escaping overflow: hidden and z-index stacking problems, while React events still bubble through the React tree.
import { createPortal } from "react-dom";
function Modal({ open, onClose, children }) {
useEffect(() => {
if (!open) return;
const onKey = (e) => e.key === "Escape" && onClose();
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [open, onClose]);
if (!open) return null;
return createPortal(
<div className="backdrop" onClick={onClose}>
<div role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.body,
);
}๐ป Pattern 7 โ Error Boundaries
Catch render errors in a subtree and show a fallback. Still class-only (or use react-error-boundary).
class ErrorBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) { return { error }; }
componentDidCatch(error, info) { logToSentry(error, info.componentStack); }
render() {
if (this.state.error) {
return this.props.fallback ?? (
<div role="alert">
Something went wrong. <button onClick={() => this.setState({ error: null })}>Retry</button>
</div>
);
}
return this.props.children;
}
}They do NOT catch: errors in event handlers, async code (setTimeout, promises), server-side rendering, or errors in the boundary itself. Handle those with try/catch and state.
๐ป Pattern 8 โ Polymorphic as Prop
function Text({ as: Component = "p", ...props }) {
return <Component {...props} />;
}
<Text as="h1">Title</Text>;
<Text as={Link} to="/about">About</Text>;๐ป Pattern 9 โ Container / Presentational (With Hooks)
// Logic in a hook
function useOrders() {
return useQuery({ queryKey: ["orders"], queryFn: fetchOrders });
}
// Pure presentational component โ easy to test and reuse in Storybook
function OrderTable({ orders }) { /* only renders */ }
function OrdersPage() {
const { data, isLoading } = useOrders();
return isLoading ? <Spinner /> : <OrderTable orders={data} />;
}๐ Real-World Use
| Pattern | Typical use |
|---|---|
| Compound | Tabs, accordions, dropdown menus, design-system components |
| Render props / hooks | Data fetching, mouse/scroll tracking, virtual lists |
| HOC | Auth guards, analytics wrappers, legacy code |
| Controlled + uncontrolled | Reusable form inputs, toggles, date pickers |
| State reducer | Highly customisable library components (Downshift) |
| Portal | Modals, toasts, tooltips, dropdowns inside scroll containers |
| Error Boundary | Per route and per widget so one crash does not blank the app |
๐ฏ Likely Interview Questions
- What are compound components? Build a Tabs or Accordion with them.
- Render props vs HOC vs custom hooks โ when would you use each today?
- How do you build a component that can be controlled or uncontrolled?
- What is a portal and why do modals use it? Do events bubble through portals?
- What do Error Boundaries not catch? How do you handle those errors?
- Why are Error Boundaries still class components?
- What is the state-reducer pattern?
โ Concurrent Rendering & Suspense | Index | Next: Server State & Data Fetching โ