Module 03: Advanced Component Patterns β Production-Grade React
Estimated Time: 15-20 hours Difficulty: π΄ Advanced Prerequisites: Module 01 (Components, Props), Module 02 (Hooks) Interview Weight: ββββ (Frequently asked in senior interviews)
π Table of Contents
- Why Patterns Matter
- Higher-Order Components (HOCs)
- Render Props
- Compound Components
- Controlled vs Uncontrolled Component Patterns
- Provider Pattern
- Container/Presentational Pattern
- Headless Components
- Error Boundaries
- Portals
- Forwarding Refs
- Common Mistakes & Gotchas
- Practice Problems
- Interview Questions
- "What's the Output?" Challenges
1. Why Patterns Matter
Real-World Analogy
Patterns are like architectural blueprints. A builder could figure out how to construct a house from scratch every time, but using proven blueprints saves time, prevents mistakes, and produces better results.
In React, patterns solve recurring problems:
- How do I share behavior between components?
- How do I build flexible, reusable component APIs?
- How do I separate concerns cleanly?
- How do I handle cross-cutting concerns (auth, logging, theming)?
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Evolution of Code Sharing in React: β
β β
β 2013-2016: Mixins (class-based, abandoned) β
β β β
β 2016-2018: Higher-Order Components (HOCs) β
β β β
β 2017-2019: Render Props β
β β β
β 2019-now: Custom Hooks (current standard) β
β β
β All three patterns (HOCs, Render Props, Hooks) β
β are still used in production. You need to know all. β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ2. Higher-Order Components (HOCs)
What is an HOC?
A Higher-Order Component is a function that takes a component and returns a new component with additional behavior. It's the component equivalent of a higher-order function.
HOC Function:
Input: Component (WrappedComponent)
Output: Component (EnhancedComponent)
WrappedComponent β [HOC] β EnhancedComponent
β
βββ Has all original functionality
βββ Plus new behavior added by HOCBasic HOC Example
// HOC that adds loading state to any component
function withLoading(WrappedComponent) {
// Return a new component
return function WithLoadingComponent({ isLoading, ...rest }) {
if (isLoading) {
return (
<div style={{
display: 'flex', justifyContent: 'center',
alignItems: 'center', padding: '40px'
}}>
<div className="spinner" />
<p>Loading...</p>
</div>
);
}
// Pass through all other props to the wrapped component
return <WrappedComponent {...rest} />;
};
}
// Original component β knows nothing about loading
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// Enhanced component β now handles loading!
const UserListWithLoading = withLoading(UserList);
// Usage:
function App() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => { setUsers(data); setLoading(false); });
}, []);
return <UserListWithLoading isLoading={loading} users={users} />;
}Real-World HOC: withAuth
function withAuth(WrappedComponent) {
return function WithAuthComponent(props) {
const { user, loading } = useAuth(); // From our auth context (Module 02)
if (loading) {
return <LoadingSpinner />;
}
if (!user) {
return <Navigate to="/login" replace />;
}
// Pass user as an additional prop
return <WrappedComponent {...props} user={user} />;
};
}
// Usage:
function Dashboard({ user }) {
return <h1>Welcome, {user.name}!</h1>;
}
const ProtectedDashboard = withAuth(Dashboard);
// <ProtectedDashboard /> β automatically redirects if not logged inHOC Conventions and Best Practices
// Convention 1: Name starts with "with"
const EnhancedComponent = withAuth(BaseComponent);
const EnhancedComponent = withTheme(BaseComponent);
const EnhancedComponent = withLoading(BaseComponent);
// Convention 2: Pass through unrelated props
function withLogger(WrappedComponent) {
return function WithLogger(props) {
useEffect(() => {
console.log(`${WrappedComponent.name} mounted`);
}, []);
// β
Pass ALL props through β don't "eat" props
return <WrappedComponent {...props} />;
};
}
// Convention 3: Set displayName for debugging
function withAuth(WrappedComponent) {
function WithAuth(props) { /* ... */ }
WithAuth.displayName = `withAuth(${
WrappedComponent.displayName || WrappedComponent.name || 'Component'
})`;
return WithAuth;
}
// Convention 4: Don't call HOCs inside render
function App() {
// β Creates a new component type every render!
const Enhanced = withAuth(MyComponent); // BAD β inside component body
return <Enhanced />;
}
// β
Call HOCs outside of component body
const Enhanced = withAuth(MyComponent);
function App() {
return <Enhanced />;
}π’ Real-World: Where HOCs Are Used
Libraries using HOCs:
βββ React Redux: connect(mapState, mapDispatch)(Component)
βββ React Router (v5): withRouter(Component)
βββ Material-UI: withStyles(styles)(Component)
βββ Apollo GraphQL: graphql(query)(Component)
βββ React DnD: DragSource(spec)(Component)HOC Limitations
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HOC Problems: β
β β
β 1. Wrapper Hell: β
β export default withAuth( β
β withTheme( β
β withRouter( β
β withLoading(MyComponent) β
β ) β
β ) β
β ); β
β // 4 wrapper components = deeply nested tree! β
β β
β 2. Prop Name Collisions: β
β // Both HOCs inject a prop called "data" β
β withUserData(withProductData(MyComponent)) β
β // Which "data" prop does MyComponent get? β
β β
β 3. Indirection: β
β // Where does this prop come from? Hard to trace β
β function MyComponent({ user, theme, router }) { β
β // Are these from props, HOCs, or context? β
β } β
β β
β 4. Static Composition Only: β
β // HOCs are applied at definition time, not β
β // at render time. Can't dynamically choose. β
β β
β These problems led to Render Props, then Hooks. β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ3. Render Props
What Are Render Props?
A render prop is a function prop that a component uses to know what to render. It gives the parent control over the child's rendering.
// The component accepts a function as a prop
// and calls it to determine what to render
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
// Call the render prop function with the data
return render(position);
}
// Usage β the CONSUMER decides how to render the data
function App() {
return (
<div>
{/* Display as text */}
<MouseTracker
render={({ x, y }) => (
<p>Mouse is at ({x}, {y})</p>
)}
/>
{/* Display as a following dot */}
<MouseTracker
render={({ x, y }) => (
<div style={{
width: 20, height: 20, borderRadius: '50%',
backgroundColor: 'red',
position: 'fixed', left: x - 10, top: y - 10,
pointerEvents: 'none',
}} />
)}
/>
</div>
);
}Children as a Function (Common Variation)
// Instead of a "render" prop, use children as a function
function DataFetcher({ url, children }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
fetch(url)
.then(res => res.json())
.then(data => { if (!cancelled) { setData(data); setLoading(false); }})
.catch(err => { if (!cancelled) { setError(err); setLoading(false); }});
return () => { cancelled = true; };
}, [url]);
// Call children as a function
return children({ data, loading, error });
}
// Usage:
function UserPage() {
return (
<DataFetcher url="/api/users">
{({ data, loading, error }) => {
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return (
<ul>
{data.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}}
</DataFetcher>
);
}π HOCs vs Render Props vs Custom Hooks
| Feature | HOC | Render Props | Custom Hooks |
|---|---|---|---|
| Syntax | withX(Component) | <X render={fn}> | useX() |
| Wrapper element? | Yes (extra component) | Yes (extra component) | No |
| Prop collision? | Possible | No | N/A |
| Dynamic? | No (static) | Yes (per render) | Yes |
| Composability | Chaining | Nesting | Simple calls |
| Debugging | Hard (indirection) | Moderate | Easy |
| Modern React? | Legacy (still used) | Used in libraries | β Preferred |
| TypeScript? | Hard to type | Moderate | Easy to type |
When to Use Each
Custom Hooks β Default choice for sharing logic
Render Props β When the consumer needs to control rendering
HOCs β When wrapping with cross-cutting concerns (rare now)4. Compound Components
What Are Compound Components?
Compound Components is a pattern where multiple components work together to form a complete UI, sharing implicit state. Think of <select> and <option> in HTML β they're separate elements but work together.
Real-World Analogy
A TV remote control: The remote (parent) and buttons (children) work as a unit. The buttons don't work without the remote, and the remote is useless without buttons. They share an implicit connection.
Basic Example: Custom Tab Component
import { createContext, useContext, useState } from 'react';
// Shared context between parent and children
const TabContext = createContext();
// Parent component β manages the state
function Tabs({ children, defaultIndex = 0, onChange }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
const handleChange = (index) => {
setActiveIndex(index);
onChange?.(index);
};
return (
<TabContext.Provider value={{ activeIndex, setActiveIndex: handleChange }}>
<div className="tabs">{children}</div>
</TabContext.Provider>
);
}
// Tab list container
function TabList({ children }) {
return (
<div className="tab-list" role="tablist" style={{
display: 'flex', borderBottom: '2px solid #e0e0e0',
}}>
{/* Inject index into each Tab child */}
{React.Children.map(children, (child, index) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { index });
}
return child;
})}
</div>
);
}
// Individual tab button
function Tab({ children, index, disabled = false }) {
const { activeIndex, setActiveIndex } = useContext(TabContext);
const isActive = activeIndex === index;
return (
<button
role="tab"
aria-selected={isActive}
disabled={disabled}
onClick={() => !disabled && setActiveIndex(index)}
style={{
padding: '12px 24px',
border: 'none',
borderBottom: isActive ? '3px solid #2196F3' : '3px solid transparent',
backgroundColor: 'transparent',
color: isActive ? '#2196F3' : disabled ? '#ccc' : '#666',
fontWeight: isActive ? 'bold' : 'normal',
cursor: disabled ? 'not-allowed' : 'pointer',
fontSize: '15px',
transition: 'all 0.2s ease',
}}
>
{children}
</button>
);
}
// Tab panels container
function TabPanels({ children }) {
const { activeIndex } = useContext(TabContext);
return (
<div className="tab-panels" style={{ padding: '20px' }}>
{React.Children.toArray(children)[activeIndex]}
</div>
);
}
// Individual tab panel
function TabPanel({ children }) {
return <div role="tabpanel">{children}</div>;
}
// Attach sub-components to parent
Tabs.TabList = TabList;
Tabs.Tab = Tab;
Tabs.TabPanels = TabPanels;
Tabs.TabPanel = TabPanel;
// Usage β clean, declarative API:
function App() {
return (
<Tabs defaultIndex={0} onChange={(i) => console.log('Tab changed:', i)}>
<Tabs.TabList>
<Tabs.Tab>Profile</Tabs.Tab>
<Tabs.Tab>Settings</Tabs.Tab>
<Tabs.Tab disabled>Admin</Tabs.Tab>
</Tabs.TabList>
<Tabs.TabPanels>
<Tabs.TabPanel>
<h2>Profile Content</h2>
<p>Your profile information goes here.</p>
</Tabs.TabPanel>
<Tabs.TabPanel>
<h2>Settings Content</h2>
<p>Application settings go here.</p>
</Tabs.TabPanel>
<Tabs.TabPanel>
<h2>Admin Content</h2>
<p>Admin panel content.</p>
</Tabs.TabPanel>
</Tabs.TabPanels>
</Tabs>
);
}π’ Libraries Using Compound Components
Real-world examples:
βββ Radix UI: <Select.Root><Select.Trigger><Select.Content>
βββ Headless UI: <Menu><Menu.Button><Menu.Items><Menu.Item>
βββ Reach UI: <Accordion><AccordionItem><AccordionButton>
βββ Chakra UI: <Menu><MenuButton><MenuList><MenuItem>
βββ React Bootstrap: <Dropdown><Dropdown.Toggle><Dropdown.Menu>Benefits of Compound Components
1. Flexible API β consumer controls structure and layout
2. Implicit state sharing β no prop drilling between parts
3. Intuitive usage β reads like describing a UI
4. Easy to extend β add new sub-components without changing parent
5. Separation of concerns β each sub-component handles its own rendering5. Controlled vs Uncontrolled Component Patterns
The Concept
Controlled: Parent owns the state, child reports changes
Uncontrolled: Child owns its own state, parent reads when needed
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Controlled: β
β ββββββββββββββββββ ββββββββββββββββββββ β
β β Parent β β Child β β
β β state: "hello" ββββββΆβ value={state} β β
β β setState(...) βββββββ onChange={fn} β β
β ββββββββββββββββββ ββββββββββββββββββββ β
β Parent is the "single source of truth" β
β β
β Uncontrolled: β
β ββββββββββββββββββ ββββββββββββββββββββ β
β β Parent β β Child β β
β β (no state) β β own state: "hi" β β
β β reads via ref ββββββΆβ ref={ref} β β
β ββββββββββββββββββ ββββββββββββββββββββ β
β Child manages itself; parent reads when needed β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββBuilding a Component That Supports Both
import { useState, useRef, useEffect } from 'react';
// This Toggle supports BOTH controlled and uncontrolled usage
function Toggle({
// Controlled props
isOn: controlledIsOn,
onChange,
// Uncontrolled props
defaultIsOn = false,
}) {
// Determine if we're in controlled mode
const isControlled = controlledIsOn !== undefined;
// Internal state for uncontrolled mode
const [internalIsOn, setInternalIsOn] = useState(defaultIsOn);
// Use controlled value if provided, otherwise use internal
const isOn = isControlled ? controlledIsOn : internalIsOn;
const handleToggle = () => {
const newValue = !isOn;
if (!isControlled) {
setInternalIsOn(newValue); // Update internal state
}
onChange?.(newValue); // Notify parent either way
};
return (
<button
onClick={handleToggle}
role="switch"
aria-checked={isOn}
style={{
width: '60px', height: '30px', borderRadius: '15px',
border: 'none', cursor: 'pointer',
backgroundColor: isOn ? '#4CAF50' : '#ccc',
position: 'relative', transition: 'background-color 0.3s',
}}
>
<span style={{
width: '26px', height: '26px', borderRadius: '50%',
backgroundColor: 'white', position: 'absolute',
top: '2px', left: isOn ? '32px' : '2px',
transition: 'left 0.3s',
}} />
</button>
);
}
// Uncontrolled usage β Toggle manages its own state
function App() {
return (
<Toggle
defaultIsOn={false}
onChange={(isOn) => console.log('Toggled:', isOn)}
/>
);
}
// Controlled usage β Parent manages the state
function App() {
const [darkMode, setDarkMode] = useState(false);
return (
<Toggle
isOn={darkMode}
onChange={(isOn) => setDarkMode(isOn)}
/>
);
}π― Interview Tip: Many well-designed component libraries (Radix, MUI, Chakra) support both controlled and uncontrolled modes. Knowing how to build this pattern demonstrates senior-level thinking.
6. Provider Pattern
The Provider pattern wraps a subtree with context, providing data and functionality to all descendants.
// A complete theme provider with system preference detection
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
const ThemeContext = createContext(undefined);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
// Check localStorage, then system preference
const saved = localStorage.getItem('theme');
if (saved) return saved;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
});
// Persist to localStorage
useEffect(() => {
localStorage.setItem('theme', theme);
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
// Listen for system theme changes
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e) => {
if (!localStorage.getItem('theme')) {
setTheme(e.matches ? 'dark' : 'light');
}
};
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, []);
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
}, []);
const value = { theme, setTheme, toggleTheme, isDark: theme === 'dark' };
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// Usage:
function App() {
return (
<ThemeProvider>
<Header />
<Main />
</ThemeProvider>
);
}
function Header() {
const { isDark, toggleTheme } = useTheme();
return (
<header style={{ background: isDark ? '#1a1a1a' : '#ffffff' }}>
<button onClick={toggleTheme}>
{isDark ? 'βοΈ' : 'π'}
</button>
</header>
);
}Provider Composition
// Instead of deeply nesting providers:
// β
<AuthProvider>
<ThemeProvider>
<I18nProvider>
<NotificationProvider>
<App />
</NotificationProvider>
</I18nProvider>
</ThemeProvider>
</AuthProvider>
// β
Create a compose helper:
function ComposeProviders({ providers, children }) {
return providers.reduceRight(
(acc, [Provider, props]) => <Provider {...props}>{acc}</Provider>,
children
);
}
// Usage:
function Root() {
return (
<ComposeProviders
providers={[
[AuthProvider, {}],
[ThemeProvider, { defaultTheme: 'dark' }],
[I18nProvider, { locale: 'en' }],
[NotificationProvider, {}],
]}
>
<App />
</ComposeProviders>
);
}7. Container/Presentational Pattern
The Concept
Separate components into:
- Container (smart): Handles data fetching, state, business logic
- Presentational (dumb): Only renders UI based on props, no state or logic
// Presentational Component β only cares about how things look
function UserList({ users, onDelete, isLoading }) {
if (isLoading) {
return <div className="skeleton-list">Loading...</div>;
}
return (
<ul className="user-list">
{users.map(user => (
<li key={user.id} className="user-item">
<img src={user.avatar} alt={user.name} />
<span>{user.name}</span>
<span>{user.email}</span>
<button onClick={() => onDelete(user.id)}>Delete</button>
</li>
))}
</ul>
);
}
// Container Component β handles data and logic
function UserListContainer() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => { setUsers(data); setIsLoading(false); });
}, []);
const handleDelete = async (userId) => {
await fetch(`/api/users/${userId}`, { method: 'DELETE' });
setUsers(prev => prev.filter(u => u.id !== userId));
};
return (
<UserList
users={users}
isLoading={isLoading}
onDelete={handleDelete}
/>
);
}π‘ Modern Note: With custom hooks, this pattern is less common. You can just use
useUsers()hook in the component. But the concept of separating logic from UI is still valuable.
8. Headless Components
The Concept
Headless components provide behavior and state management without any UI. They use hooks or render props to let the consumer fully control the rendering.
// Headless Toggle β zero UI, all logic
function useToggle(initialState = false) {
const [isOn, setIsOn] = useState(initialState);
const toggle = useCallback(() => setIsOn(prev => !prev), []);
const setOn = useCallback(() => setIsOn(true), []);
const setOff = useCallback(() => setIsOn(false), []);
const getToggleProps = (props = {}) => ({
'aria-pressed': isOn,
onClick: (...args) => {
toggle();
props.onClick?.(...args);
},
...props,
});
return { isOn, toggle, setOn, setOff, getToggleProps };
}
// Consumer controls ALL the rendering:
function FancyToggle() {
const { isOn, getToggleProps } = useToggle();
return (
<button
{...getToggleProps()}
style={{
background: isOn ? 'linear-gradient(135deg, #667eea, #764ba2)' : '#e0e0e0',
color: isOn ? 'white' : '#666',
padding: '12px 32px',
borderRadius: '25px',
border: 'none',
fontSize: '16px',
cursor: 'pointer',
transition: 'all 0.3s ease',
}}
>
{isOn ? 'π Activated' : 'Activate'}
</button>
);
}
// SAME hook, completely different UI:
function AccessibilityToggle() {
const { isOn, getToggleProps } = useToggle();
return (
<label>
<input type="checkbox" {...getToggleProps()} checked={isOn} readOnly />
High contrast mode: {isOn ? 'ON' : 'OFF'}
</label>
);
}π’ Popular Headless Libraries
βββ TanStack Table (React Table) β headless data tables
βββ Downshift β headless autocomplete/select
βββ Headless UI (Tailwind) β headless accessible components
βββ React Aria (Adobe) β headless accessible primitives
βββ Radix UI β both headless and styled options
βββ React Hook Form β headless form management9. Error Boundaries
The Problem
When a JavaScript error occurs inside a component, it used to crash the entire app. Error Boundaries let you catch errors in a subtree and show a fallback UI.
β οΈ Error Boundaries are the ONE thing that still requires class components (as of React 18). There is no hook equivalent. React 19 will introduce error boundary hooks.
import { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
// Called when an error is thrown during rendering
static getDerivedStateFromError(error) {
// Update state so next render shows fallback UI
return { hasError: true, error };
}
// Called after an error has been thrown β good for logging
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error);
console.error('Component stack:', errorInfo.componentStack);
// π’ In production, send to error tracking service
// Sentry.captureException(error, { extra: errorInfo });
// LogRocket.captureException(error);
this.setState({ errorInfo });
}
render() {
if (this.state.hasError) {
// Render fallback UI
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div style={{
padding: '40px', textAlign: 'center',
backgroundColor: '#fff3f3', borderRadius: '8px',
border: '1px solid #ffcdd2', margin: '20px',
}}>
<h2>β οΈ Something went wrong</h2>
<p style={{ color: '#666' }}>
{this.state.error?.message || 'An unexpected error occurred'}
</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
style={{
padding: '10px 24px', backgroundColor: '#2196F3',
color: 'white', border: 'none', borderRadius: '6px',
cursor: 'pointer', fontSize: '16px',
}}
>
Try Again
</button>
{process.env.NODE_ENV === 'development' && this.state.errorInfo && (
<details style={{ marginTop: '20px', textAlign: 'left' }}>
<summary>Error details</summary>
<pre style={{ fontSize: '12px', overflow: 'auto' }}>
{this.state.error?.toString()}
{this.state.errorInfo.componentStack}
</pre>
</details>
)}
</div>
);
}
return this.props.children;
}
}
// Usage:
function App() {
return (
<ErrorBoundary>
<Header />
<ErrorBoundary fallback={<p>Failed to load sidebar</p>}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<p>Failed to load content</p>}>
<MainContent />
</ErrorBoundary>
</ErrorBoundary>
);
}What Error Boundaries DON'T Catch
Error Boundaries catch:
β
Errors during rendering
β
Errors in lifecycle methods
β
Errors in constructors of child components
Error Boundaries do NOT catch:
β Event handlers (use try/catch instead)
β Asynchronous code (setTimeout, fetch, promises)
β Server-side rendering errors
β Errors thrown in the error boundary itself10. Portals
What Are Portals?
Portals let you render a child component into a different DOM node, outside the parent's DOM hierarchy, while maintaining the React component hierarchy (events, context still work).
import { createPortal } from 'react-dom';
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
// Render into document.body instead of the parent's DOM
return createPortal(
<div
className="modal-overlay"
onClick={onClose}
style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 1000,
}}
>
<div
className="modal-content"
onClick={(e) => e.stopPropagation()}
style={{
backgroundColor: 'white', padding: '32px',
borderRadius: '12px', maxWidth: '500px',
width: '90%', maxHeight: '80vh', overflow: 'auto',
}}
>
<button
onClick={onClose}
style={{
position: 'absolute', top: '12px', right: '12px',
border: 'none', background: 'none', fontSize: '24px',
cursor: 'pointer',
}}
>
Γ
</button>
{children}
</div>
</div>,
document.body // Render here instead of parent DOM
);
}
// Usage:
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div style={{ overflow: 'hidden', height: '200px' }}>
{/* Even though parent has overflow: hidden,
the modal appears on top of everything
because it's rendered into document.body! */}
<button onClick={() => setShowModal(true)}>Open Modal</button>
<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
<h2>Hello from a Portal!</h2>
<p>I'm rendered outside my parent's DOM.</p>
</Modal>
</div>
);
}When to Use Portals
Use Cases:
βββ Modals / Dialogs β avoid parent overflow/z-index issues
βββ Tooltips β position relative to viewport, not parent
βββ Dropdown menus β escape parent clipping
βββ Toast notifications β always visible at fixed position
βββ Full-screen overlays11. Forwarding Refs
The Problem
Refs don't pass through components automatically:
// β This doesn't work β ref points to MyInput, not the <input>
function MyInput(props) {
return <input {...props} />;
}
const ref = useRef();
<MyInput ref={ref} /> // ref.current is undefined!The Solution: forwardRef
import { forwardRef, useRef } from 'react';
// Wrap your component with forwardRef
const FancyInput = forwardRef(function FancyInput({ label, ...props }, ref) {
return (
<div className="fancy-input">
<label>{label}</label>
<input ref={ref} {...props} /> {/* Forward the ref to the actual input */}
</div>
);
});
// Usage:
function Form() {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus(); // β
Works! ref points to the <input> DOM element
};
return (
<div>
<FancyInput ref={inputRef} label="Username" />
<button onClick={handleFocus}>Focus Input</button>
</div>
);
}useImperativeHandle β Control What Parent Can Do
import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) {
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
// Customize what the parent can access via ref
useImperativeHandle(ref, () => ({
play() {
videoRef.current.play();
setIsPlaying(true);
},
pause() {
videoRef.current.pause();
setIsPlaying(false);
},
get isPlaying() {
return isPlaying;
},
// Parent CANNOT access the raw video element
// We only expose a controlled API
}), [isPlaying]);
return <video ref={videoRef} src={src} />;
});
// Usage:
function App() {
const playerRef = useRef(null);
return (
<div>
<VideoPlayer ref={playerRef} src="/video.mp4" />
<button onClick={() => playerRef.current.play()}>Play</button>
<button onClick={() => playerRef.current.pause()}>Pause</button>
</div>
);
}12. Common Mistakes & Gotchas
Mistake 1: Creating HOCs Inside Render
// β New component type every render β unmounts and remounts!
function App() {
const Enhanced = withLoading(UserList); // Creates new component every render
return <Enhanced isLoading={loading} />;
}
// β
Create HOC outside the component
const EnhancedUserList = withLoading(UserList);
function App() {
return <EnhancedUserList isLoading={loading} />;
}Mistake 2: Forgetting Error Boundary Limitations
// β Error boundaries don't catch event handler errors!
function Button() {
const handleClick = () => {
throw new Error('Crash!'); // Not caught by error boundary!
};
return <button onClick={handleClick}>Click</button>;
}
// β
Use try/catch for event handlers
function Button() {
const [error, setError] = useState(null);
const handleClick = () => {
try {
doSomethingRisky();
} catch (err) {
setError(err);
}
};
if (error) return <p>Error: {error.message}</p>;
return <button onClick={handleClick}>Click</button>;
}Mistake 3: Over-Engineering with Patterns
// β Don't use a pattern just because it exists
// A simple component doesn't need compound components, HOCs, or render props
// β Over-engineered
const EnhancedButton = withTheme(withTracking(withAuth(Button)));
// β
Just use hooks
function Button() {
const theme = useTheme();
const track = useTracking();
const auth = useAuth();
return <button>Click</button>;
}13. Practice Problems
π§ͺ Problem 1: Build a Disclosure/Accordion with Compound Components
Build a Disclosure component using the compound component pattern with these sub-components: Disclosure.Button, Disclosure.Panel.
import { createContext, useContext, useState } from 'react';
const DisclosureContext = createContext();
function Disclosure({ children, defaultOpen = false }) {
const [isOpen, setIsOpen] = useState(defaultOpen);
const toggle = () => setIsOpen(prev => !prev);
return (
<DisclosureContext.Provider value={{ isOpen, toggle }}>
<div style={{ border: '1px solid #e0e0e0', borderRadius: '8px', overflow: 'hidden' }}>
{children}
</div>
</DisclosureContext.Provider>
);
}
Disclosure.Button = function DisclosureButton({ children }) {
const { isOpen, toggle } = useContext(DisclosureContext);
return (
<button onClick={toggle} style={{
width: '100%', padding: '16px', textAlign: 'left',
backgroundColor: isOpen ? '#f5f5f5' : '#fff',
border: 'none', cursor: 'pointer', fontSize: '16px',
display: 'flex', justifyContent: 'space-between',
}}>
{children}
<span style={{ transform: isOpen ? 'rotate(180deg)' : '', transition: '0.2s' }}>
βΌ
</span>
</button>
);
};
Disclosure.Panel = function DisclosurePanel({ children }) {
const { isOpen } = useContext(DisclosureContext);
if (!isOpen) return null;
return (
<div style={{ padding: '16px', borderTop: '1px solid #e0e0e0', background: '#fafafa' }}>
{children}
</div>
);
};
// Usage:
function FAQ() {
return (
<div style={{ maxWidth: '600px', margin: '20px auto' }}>
<Disclosure>
<Disclosure.Button>What is React?</Disclosure.Button>
<Disclosure.Panel>
React is a JavaScript library for building user interfaces.
</Disclosure.Panel>
</Disclosure>
<Disclosure defaultOpen>
<Disclosure.Button>Why use hooks?</Disclosure.Button>
<Disclosure.Panel>
Hooks let you use state and lifecycle in function components.
</Disclosure.Panel>
</Disclosure>
</div>
);
}π§ͺ Problem 2: Build a withLogger HOC
Create an HOC that logs when a component mounts, unmounts, and receives new props.
<details> <summary>β Solution</summary>import { useEffect, useRef } from 'react';
function withLogger(WrappedComponent) {
function WithLogger(props) {
const prevPropsRef = useRef(props);
const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
useEffect(() => {
console.log(`[${componentName}] Mounted with props:`, props);
return () => {
console.log(`[${componentName}] Unmounted`);
};
}, []);
useEffect(() => {
const prevProps = prevPropsRef.current;
const changedProps = {};
Object.keys(props).forEach(key => {
if (prevProps[key] !== props[key]) {
changedProps[key] = { from: prevProps[key], to: props[key] };
}
});
if (Object.keys(changedProps).length > 0) {
console.log(`[${componentName}] Props changed:`, changedProps);
}
prevPropsRef.current = props;
});
return <WrappedComponent {...props} />;
}
WithLogger.displayName = `withLogger(${
WrappedComponent.displayName || WrappedComponent.name || 'Component'
})`;
return WithLogger;
}
// Usage:
function UserCard({ name, age }) {
return <div>{name} ({age})</div>;
}
const LoggedUserCard = withLogger(UserCard);π§ͺ Problem 3: Build a Notification Toast System with Portals
Build a toast notification system using portals that can show/dismiss messages.
<details> <summary>β Solution</summary>import { createContext, useContext, useState, useCallback } from 'react';
import { createPortal } from 'react-dom';
const ToastContext = createContext();
function ToastProvider({ children }) {
const [toasts, setToasts] = useState([]);
const addToast = useCallback((message, type = 'info', duration = 3000) => {
const id = Date.now() + Math.random();
setToasts(prev => [...prev, { id, message, type }]);
if (duration > 0) {
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, duration);
}
return id;
}, []);
const removeToast = useCallback((id) => {
setToasts(prev => prev.filter(t => t.id !== id));
}, []);
const toast = {
info: (msg, dur) => addToast(msg, 'info', dur),
success: (msg, dur) => addToast(msg, 'success', dur),
error: (msg, dur) => addToast(msg, 'error', dur),
warning: (msg, dur) => addToast(msg, 'warning', dur),
};
const colors = { info: '#2196F3', success: '#4CAF50', error: '#f44336', warning: '#ff9800' };
return (
<ToastContext.Provider value={toast}>
{children}
{createPortal(
<div style={{
position: 'fixed', top: '20px', right: '20px',
display: 'flex', flexDirection: 'column', gap: '10px', zIndex: 9999,
}}>
{toasts.map(t => (
<div key={t.id} style={{
padding: '14px 20px', borderRadius: '8px',
backgroundColor: colors[t.type], color: 'white',
boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
display: 'flex', alignItems: 'center', gap: '12px',
minWidth: '280px', animation: 'slideIn 0.3s ease',
}}>
<span style={{ flex: 1 }}>{t.message}</span>
<button onClick={() => removeToast(t.id)} style={{
background: 'none', border: 'none', color: 'white',
fontSize: '18px', cursor: 'pointer',
}}>Γ</button>
</div>
))}
</div>,
document.body
)}
</ToastContext.Provider>
);
}
function useToast() {
const context = useContext(ToastContext);
if (!context) throw new Error('useToast must be used within ToastProvider');
return context;
}
// Usage:
function App() {
return (
<ToastProvider>
<Dashboard />
</ToastProvider>
);
}
function Dashboard() {
const toast = useToast();
return (
<div>
<button onClick={() => toast.success('File saved!')}>Save</button>
<button onClick={() => toast.error('Network error!')}>Error</button>
<button onClick={() => toast.info('New update available')}>Info</button>
<button onClick={() => toast.warning('Low disk space')}>Warn</button>
</div>
);
}14. Interview Questions
β Q1: What is the compound component pattern? Why use it?
Answer: Compound components are a set of components that work together to form a complete UI, sharing implicit state through Context. Like <select> and <option> in HTML.
Benefits: Flexible consumer API, implicit state sharing without prop drilling, intuitive JSX structure, easy to extend. Used by Radix UI, Headless UI, Chakra UI, and most component libraries.
β Q2: Explain the difference between HOCs, Render Props, and Custom Hooks.
Answer: All three solve the same problem β sharing behavior between components β but with different approaches:
- HOC: Wraps component at definition time. Returns enhanced component. Can cause wrapper hell and prop collisions.
- Render Props: Passes data via a function prop at render time. More flexible but can create "callback hell."
- Custom Hooks: Extract logic into a reusable function. No wrapper components, no nesting, clean composition. The modern standard.
β Q3: What are Error Boundaries? Why can't they be function components?
Answer: Error Boundaries catch JavaScript errors in their child component tree and display fallback UI. They require the componentDidCatch and getDerivedStateFromError lifecycle methods, which only exist in class components. React hasn't released hook equivalents yet (planned for a future version).
They don't catch errors in: event handlers, async code, SSR, or the boundary itself.
β Q4: What are Portals? When would you use them?
Answer: Portals (createPortal(child, container)) render children into a DOM node outside the parent hierarchy. Event bubbling and context still work through the React tree.
Use for: modals, tooltips, dropdown menus, toast notifications β anything that needs to "escape" parent CSS (overflow, z-index, positioning).
β Q5: What is forwardRef? When do you need it?
Answer: forwardRef lets a component pass its ref to a child element. Without it, refs on custom components don't work. Combined with useImperativeHandle, you can control exactly what the parent can access via the ref.
15. "What's the Output?" Challenges
Challenge 1
const Button = forwardRef(function Button({ children }, ref) {
console.log('ref inside Button:', ref);
return <button ref={ref}>{children}</button>;
});
function App() {
const ref = useRef(null);
useEffect(() => {
console.log('ref.current:', ref.current);
console.log('tagName:', ref.current?.tagName);
}, []);
return <Button ref={ref}>Click me</Button>;
}ref inside Button: { current: null } (during render, ref not attached yet)
ref.current: <button>Click me</button> (after mount, ref is attached)
tagName: BUTTONChallenge 2
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
render() {
if (this.state.hasError) return <p>Error!</p>;
return this.props.children;
}
}
function Broken() {
throw new Error('Crash!');
}
function App() {
const [show, setShow] = useState(false);
return (
<ErrorBoundary>
<button onClick={() => setShow(true)}>Show</button>
{show && <Broken />}
</ErrorBoundary>
);
}
// What happens when you click "Show"?The entire content inside ErrorBoundary is replaced with <p>Error!</p>.
When show becomes true, React tries to render <Broken />, which throws during rendering. ErrorBoundary catches it and renders the fallback. The button disappears too because the entire ErrorBoundary children are replaced by the fallback.
π― Module 03 Summary
You now understand:
- β Higher-Order Components β wrapping components with extra behavior
- β Render Props β delegating rendering to the consumer
- β Compound Components β building flexible multi-part UIs
- β Controlled vs Uncontrolled patterns β both modes in one component
- β Provider Pattern β sharing data through component trees
- β Container/Presentational β separating logic from UI
- β Headless Components β logic-only, UI-free components
- β Error Boundaries β catching and recovering from errors
- β Portals β rendering outside the DOM hierarchy
- β Forwarding Refs β passing refs through components
Next up: Module 04: State Management & Data Flow β where we'll tackle how to manage state at scale with Context, Redux, Zustand, and React Query. β