🧬 Advanced React Patterns — HOCs, Render Props & Compound Components
Senior-level React patterns: higher-order components, render props, compound components, controlled vs uncontrolled APIs, and state reducer pattern. Built for interview prep.
Senior React interviews often probe pattern fluency: when to use which, why each exists, and what hooks replaced. Here are the four that come up most.
🧰 1. Higher-Order Component (HOC)
A function that takes a component and returns a new one with extra behavior. Mostly a pre-hooks pattern.
function withAuth(Component) {
return function Wrapped(props) {
const { user } = useAuth();
if (!user) return <Login />;
return <Component {...props} user={user} />;
};
}🎭 2. Render Props
Pass a function as a prop (often children); the component calls it with internal state.
<Toggle>
{({ on, toggle }) => (
<button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>
)}
</Toggle>🧩 3. Compound Components
A parent shares state with closely-related children via context, exposing a flexible declarative API.
<Tabs>
<Tabs.List>
<Tabs.Trigger value="a">A</Tabs.Trigger>
<Tabs.Trigger value="b">B</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="a">...</Tabs.Panel>
<Tabs.Panel value="b">...</Tabs.Panel>
</Tabs>Used by Radix, Reach UI, Headless UI, shadcn/ui.
🎛️ 4. Controlled vs Uncontrolled APIs (state reducer pattern)
A reusable component supports BOTH a controlled mode (parent owns state) and uncontrolled mode (internal state with optional defaultValue). The state reducer lets consumers customize update logic without forking.
💡 Hooks replaced most HOC/render-prop use cases
If you find yourself writing a new HOC today, ask whether a custom hook would be cleaner. Compound components and controlled/uncontrolled APIs remain very relevant — they're the foundation of every modern headless UI library.
💻 Code Examples
Compound Tabs (sketch)
const TabsContext = createContext(null);
export function Tabs({ defaultValue, children }) {
const [active, setActive] = useState(defaultValue);
return <TabsContext.Provider value={{ active, setActive }}>{children}</TabsContext.Provider>;
}
Tabs.Trigger = function Trigger({ value, children }) {
const { active, setActive } = useContext(TabsContext);
return <button data-active={active === value} onClick={() => setActive(value)}>{children}</button>;
};
Tabs.Panel = function Panel({ value, children }) {
const { active } = useContext(TabsContext);
return active === value ? <div>{children}</div> : null;
};Consumers compose <Tabs><Tabs.Trigger /><Tabs.Panel /></Tabs> declaratively.
Render prop → equivalent custom hook
// Old (render prop)
<DataLoader url="/api/x">
{({ data, loading }) => loading ? <Spinner /> : <View data={data} />}
</DataLoader>
// Modern (custom hook)
const { data, loading } = useFetch('/api/x');
return loading ? <Spinner /> : <View data={data} />;Same outcome, less indirection.
⚠️ Common Mistakes
- Reaching for HOCs in new code — usually a custom hook is clearer.
- Overusing compound components for small widgets — adds context overhead for no gain.
- Forgetting to memoize context values inside compound components — every keystroke re-renders all consumers.
- Confusing 'render props' with 'function as children' — they're the same idea, just `children` happens to be the function prop.
🎯 Interview Questions
Real questions asked at top product and service-based companies.
Q1.What is a Higher-Order Component?Advanced
Q2.What is the render props pattern?Advanced
Q3.What are compound components?Advanced
Q4.Why prefer compound components over a single config-prop component?Advanced
Q5.What is the controlled vs uncontrolled pattern in a library component?Advanced
🧠 Quick Summary
- HOCs and render props are mostly replaced by custom hooks.
- Compound components: declarative slots + shared context state.
- Powers every modern headless UI lib (Radix, Headless UI, shadcn).
- Support controlled AND uncontrolled APIs in reusable components.
- State reducer pattern lets consumers customize update logic.