03-advanced-patterns.md

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

  1. Why Patterns Matter
  2. Higher-Order Components (HOCs)
  3. Render Props
  4. Compound Components
  5. Controlled vs Uncontrolled Component Patterns
  6. Provider Pattern
  7. Container/Presentational Pattern
  8. Headless Components
  9. Error Boundaries
  10. Portals
  11. Forwarding Refs
  12. Common Mistakes & Gotchas
  13. Practice Problems
  14. Interview Questions
  15. "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 HOC

Basic 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 in

HOC 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

FeatureHOCRender PropsCustom Hooks
SyntaxwithX(Component)<X render={fn}>useX()
Wrapper element?Yes (extra component)Yes (extra component)No
Prop collision?PossibleNoN/A
Dynamic?No (static)Yes (per render)Yes
ComposabilityChainingNestingSimple calls
DebuggingHard (indirection)ModerateEasy
Modern React?Legacy (still used)Used in librariesβœ… Preferred
TypeScript?Hard to typeModerateEasy 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 rendering

5. 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 management

9. 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 itself

10. 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 overlays

11. 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.

<details> <summary>βœ… Solution</summary>
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> ); }
</details>

πŸ§ͺ 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);
</details>

πŸ§ͺ 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> ); }
</details>

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>; }
<details> <summary>Answer</summary>
ref inside Button: { current: null } (during render, ref not attached yet) ref.current: <button>Click me</button> (after mount, ref is attached) tagName: BUTTON
</details>

Challenge 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"?
<details> <summary>Answer</summary>

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.

</details>

🎯 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. β†’