Skip to content

Advanced Patterns

This chapter covers advanced React patterns and techniques for building scalable, performant applications. You'll learn about Context API, performance optimization, code splitting, and architectural patterns.

Context API - Global State

Creating a Complete Context

jsx
import { createContext, useContext, useReducer, useCallback } from 'react';

// 1. Define initial state
const initialState = {
    user: null,
    theme: 'light',
    notifications: []
};

// 2. Define actions
const actionTypes = {
    SET_USER: 'SET_USER',
    LOGOUT: 'LOGOUT',
    SET_THEME: 'SET_THEME',
    ADD_NOTIFICATION: 'ADD_NOTIFICATION',
    REMOVE_NOTIFICATION: 'REMOVE_NOTIFICATION'
};

// 3. Reducer function
function appReducer(state, action) {
    switch (action.type) {
        case actionTypes.SET_USER:
            return { ...state, user: action.payload };
        case actionTypes.LOGOUT:
            return { ...state, user: null };
        case actionTypes.SET_THEME:
            return { ...state, theme: action.payload };
        case actionTypes.ADD_NOTIFICATION:
            return {
                ...state,
                notifications: [...state.notifications, action.payload]
            };
        case actionTypes.REMOVE_NOTIFICATION:
            return {
                ...state,
                notifications: state.notifications.filter(
                    n => n.id !== action.payload
                )
            };
        default:
            return state;
    }
}

// 4. Create context
const AppContext = createContext(null);

// 5. Provider component
function AppProvider({ children }) {
    const [state, dispatch] = useReducer(appReducer, initialState);

    // Memoized actions
    const setUser = useCallback((user) => {
        dispatch({ type: actionTypes.SET_USER, payload: user });
    }, []);

    const logout = useCallback(() => {
        dispatch({ type: actionTypes.LOGOUT });
    }, []);

    const setTheme = useCallback((theme) => {
        dispatch({ type: actionTypes.SET_THEME, payload: theme });
    }, []);

    const addNotification = useCallback((notification) => {
        const id = Date.now();
        dispatch({
            type: actionTypes.ADD_NOTIFICATION,
            payload: { ...notification, id }
        });

        // Auto-remove after 5 seconds
        setTimeout(() => {
            dispatch({ type: actionTypes.REMOVE_NOTIFICATION, payload: id });
        }, 5000);
    }, []);

    const value = {
        ...state,
        setUser,
        logout,
        setTheme,
        addNotification
    };

    return (
        <AppContext.Provider value={value}>
            {children}
        </AppContext.Provider>
    );
}

// 6. Custom hook
function useApp() {
    const context = useContext(AppContext);
    if (!context) {
        throw new Error('useApp must be used within AppProvider');
    }
    return context;
}

export { AppProvider, useApp };
tsx
import {
    createContext,
    useContext,
    useReducer,
    useCallback,
    ReactNode
} from 'react';

// 1. Define types
interface User {
    id: string;
    name: string;
    email: string;
}

interface Notification {
    id: number;
    message: string;
    type: 'success' | 'error' | 'info';
}

interface AppState {
    user: User | null;
    theme: 'light' | 'dark';
    notifications: Notification[];
}

interface AppContextValue extends AppState {
    setUser: (user: User) => void;
    logout: () => void;
    setTheme: (theme: 'light' | 'dark') => void;
    addNotification: (notification: Omit<Notification, 'id'>) => void;
}

type AppAction =
    | { type: 'SET_USER'; payload: User }
    | { type: 'LOGOUT' }
    | { type: 'SET_THEME'; payload: 'light' | 'dark' }
    | { type: 'ADD_NOTIFICATION'; payload: Notification }
    | { type: 'REMOVE_NOTIFICATION'; payload: number };

// 2. Initial state
const initialState: AppState = {
    user: null,
    theme: 'light',
    notifications: []
};

// 3. Reducer function
function appReducer(state: AppState, action: AppAction): AppState {
    switch (action.type) {
        case 'SET_USER':
            return { ...state, user: action.payload };
        case 'LOGOUT':
            return { ...state, user: null };
        case 'SET_THEME':
            return { ...state, theme: action.payload };
        case 'ADD_NOTIFICATION':
            return {
                ...state,
                notifications: [...state.notifications, action.payload]
            };
        case 'REMOVE_NOTIFICATION':
            return {
                ...state,
                notifications: state.notifications.filter(
                    (n: Notification) => n.id !== action.payload
                )
            };
        default:
            return state;
    }
}

// 4. Create context
const AppContext = createContext<AppContextValue | null>(null);

// 5. Provider component
interface AppProviderProps {
    children: ReactNode;
}

function AppProvider({ children }: AppProviderProps): JSX.Element {
    const [state, dispatch] = useReducer(appReducer, initialState);

    // Memoized actions
    const setUser = useCallback((user: User): void => {
        dispatch({ type: 'SET_USER', payload: user });
    }, []);

    const logout = useCallback((): void => {
        dispatch({ type: 'LOGOUT' });
    }, []);

    const setTheme = useCallback((theme: 'light' | 'dark'): void => {
        dispatch({ type: 'SET_THEME', payload: theme });
    }, []);

    const addNotification = useCallback((notification: Omit<Notification, 'id'>): void => {
        const id = Date.now();
        dispatch({
            type: 'ADD_NOTIFICATION',
            payload: { ...notification, id }
        });

        // Auto-remove after 5 seconds
        setTimeout(() => {
            dispatch({ type: 'REMOVE_NOTIFICATION', payload: id });
        }, 5000);
    }, []);

    const value: AppContextValue = {
        ...state,
        setUser,
        logout,
        setTheme,
        addNotification
    };

    return (
        <AppContext.Provider value={value}>
            {children}
        </AppContext.Provider>
    );
}

// 6. Custom hook
function useApp(): AppContextValue {
    const context = useContext(AppContext);
    if (!context) {
        throw new Error('useApp must be used within AppProvider');
    }
    return context;
}

export { AppProvider, useApp };
export type { User, Notification, AppState };

Splitting Contexts for Performance

jsx
// Separate contexts for different concerns
const UserContext = createContext(null);
const ThemeContext = createContext(null);
const NotificationContext = createContext(null);

// Compose providers
function AppProviders({ children }) {
    return (
        <UserProvider>
            <ThemeProvider>
                <NotificationProvider>
                    {children}
                </NotificationProvider>
            </ThemeProvider>
        </UserProvider>
    );
}

// Components only re-render when their specific context changes
function UserDisplay() {
    const { user } = useUser(); // Only re-renders on user changes
    return <p>Hello, {user?.name}</p>;
}

function ThemeToggle() {
    const { theme, setTheme } = useTheme(); // Only re-renders on theme changes
    return (
        <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
            Current: {theme}
        </button>
    );
}
tsx
import { createContext, useContext, ReactNode } from 'react';

// Type definitions
interface User {
    id: string;
    name: string;
}

interface UserContextValue {
    user: User | null;
    setUser: (user: User) => void;
}

interface ThemeContextValue {
    theme: 'light' | 'dark';
    setTheme: (theme: 'light' | 'dark') => void;
}

// Separate contexts for different concerns
const UserContext = createContext<UserContextValue | null>(null);
const ThemeContext = createContext<ThemeContextValue | null>(null);

// Custom hooks with type safety
function useUser(): UserContextValue {
    const context = useContext(UserContext);
    if (!context) throw new Error('useUser must be within UserProvider');
    return context;
}

function useTheme(): ThemeContextValue {
    const context = useContext(ThemeContext);
    if (!context) throw new Error('useTheme must be within ThemeProvider');
    return context;
}

// Compose providers
interface AppProvidersProps {
    children: ReactNode;
}

function AppProviders({ children }: AppProvidersProps): JSX.Element {
    return (
        <UserProvider>
            <ThemeProvider>
                {children}
            </ThemeProvider>
        </UserProvider>
    );
}

// Components only re-render when their specific context changes
function UserDisplay(): JSX.Element {
    const { user } = useUser(); // Only re-renders on user changes
    return <p>Hello, {user?.name}</p>;
}

function ThemeToggle(): JSX.Element {
    const { theme, setTheme } = useTheme(); // Only re-renders on theme changes
    return (
        <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
            Current: {theme}
        </button>
    );
}

Performance Optimization

React.memo

Prevent unnecessary re-renders of child components:

jsx
import { memo, useState } from 'react';

// Without memo - re-renders every time parent renders
function ExpensiveList({ items }) {
    console.log('ExpensiveList rendered');
    return (
        <ul>
            {items.map(item => (
                <li key={item.id}>{item.name}</li>
            ))}
        </ul>
    );
}

// With memo - only re-renders when items change
const MemoizedList = memo(function ExpensiveList({ items }) {
    console.log('MemoizedList rendered');
    return (
        <ul>
            {items.map(item => (
                <li key={item.id}>{item.name}</li>
            ))}
        </ul>
    );
});

// Custom comparison function
const MemoizedWithCompare = memo(
    function UserCard({ user, onClick }) {
        return (
            <div onClick={onClick}>
                <h2>{user.name}</h2>
                <p>{user.email}</p>
            </div>
        );
    },
    (prevProps, nextProps) => {
        // Return true if props are equal (skip re-render)
        return prevProps.user.id === nextProps.user.id;
    }
);

// Parent component
function Parent() {
    const [count, setCount] = useState(0);
    const [items] = useState([
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' }
    ]);

    return (
        <div>
            <button onClick={() => setCount(count + 1)}>
                Count: {count}
            </button>
            {/* MemoizedList won't re-render when count changes */}
            <MemoizedList items={items} />
        </div>
    );
}
tsx
import { memo, useState, MouseEvent } from 'react';

// Type definitions
interface Item {
    id: number;
    name: string;
}

interface User {
    id: number;
    name: string;
    email: string;
}

interface ListProps {
    items: Item[];
}

interface UserCardProps {
    user: User;
    onClick: (e: MouseEvent<HTMLDivElement>) => void;
}

// Without memo - re-renders every time parent renders
function ExpensiveList({ items }: ListProps): JSX.Element {
    console.log('ExpensiveList rendered');
    return (
        <ul>
            {items.map((item: Item) => (
                <li key={item.id}>{item.name}</li>
            ))}
        </ul>
    );
}

// With memo - only re-renders when items change
const MemoizedList = memo(function ExpensiveList({ items }: ListProps): JSX.Element {
    console.log('MemoizedList rendered');
    return (
        <ul>
            {items.map((item: Item) => (
                <li key={item.id}>{item.name}</li>
            ))}
        </ul>
    );
});

// Custom comparison function
const MemoizedWithCompare = memo<UserCardProps>(
    function UserCard({ user, onClick }) {
        return (
            <div onClick={onClick}>
                <h2>{user.name}</h2>
                <p>{user.email}</p>
            </div>
        );
    },
    (prevProps: UserCardProps, nextProps: UserCardProps): boolean => {
        // Return true if props are equal (skip re-render)
        return prevProps.user.id === nextProps.user.id;
    }
);

// Parent component
function Parent(): JSX.Element {
    const [count, setCount] = useState<number>(0);
    const [items] = useState<Item[]>([
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' }
    ]);

    return (
        <div>
            <button onClick={() => setCount(count + 1)}>
                Count: {count}
            </button>
            {/* MemoizedList won't re-render when count changes */}
            <MemoizedList items={items} />
        </div>
    );
}

useMemo and useCallback

jsx
import { useMemo, useCallback, useState } from 'react';

function SearchResults({ items, query }) {
    // Memoize expensive computation
    const filteredItems = useMemo(() => {
        console.log('Filtering items...');
        return items.filter(item =>
            item.name.toLowerCase().includes(query.toLowerCase())
        );
    }, [items, query]);

    // Memoize sorted results
    const sortedItems = useMemo(() => {
        console.log('Sorting items...');
        return [...filteredItems].sort((a, b) =>
            a.name.localeCompare(b.name)
        );
    }, [filteredItems]);

    return (
        <ul>
            {sortedItems.map(item => (
                <li key={item.id}>{item.name}</li>
            ))}
        </ul>
    );
}

function ParentWithCallbacks() {
    const [items, setItems] = useState([]);

    // With useCallback - stable reference
    const handleDelete = useCallback((id) => {
        setItems(prev => prev.filter(item => item.id !== id));
    }, []); // Empty deps because we use functional update

    const handleAdd = useCallback((newItem) => {
        setItems(prev => [...prev, newItem]);
    }, []);

    return (
        <ItemList
            items={items}
            onDelete={handleDelete}
            onAdd={handleAdd}
        />
    );
}
tsx
import { useMemo, useCallback, useState } from 'react';

interface Item {
    id: number;
    name: string;
}

interface SearchResultsProps {
    items: Item[];
    query: string;
}

interface ItemListProps {
    items: Item[];
    onDelete: (id: number) => void;
    onAdd: (item: Item) => void;
}

function SearchResults({ items, query }: SearchResultsProps): JSX.Element {
    // Memoize expensive computation
    const filteredItems = useMemo((): Item[] => {
        console.log('Filtering items...');
        return items.filter((item: Item) =>
            item.name.toLowerCase().includes(query.toLowerCase())
        );
    }, [items, query]);

    // Memoize sorted results
    const sortedItems = useMemo((): Item[] => {
        console.log('Sorting items...');
        return [...filteredItems].sort((a: Item, b: Item) =>
            a.name.localeCompare(b.name)
        );
    }, [filteredItems]);

    return (
        <ul>
            {sortedItems.map((item: Item) => (
                <li key={item.id}>{item.name}</li>
            ))}
        </ul>
    );
}

function ParentWithCallbacks(): JSX.Element {
    const [items, setItems] = useState<Item[]>([]);

    // With useCallback - stable reference
    const handleDelete = useCallback((id: number): void => {
        setItems((prev: Item[]) => prev.filter((item: Item) => item.id !== id));
    }, []); // Empty deps because we use functional update

    const handleAdd = useCallback((newItem: Item): void => {
        setItems((prev: Item[]) => [...prev, newItem]);
    }, []);

    return (
        <ItemList
            items={items}
            onDelete={handleDelete}
            onAdd={handleAdd}
        />
    );
}

Virtualization for Large Lists

jsx
import { useRef, useState } from 'react';

function VirtualizedList({ items, itemHeight = 50, windowHeight = 400 }) {
    const [scrollTop, setScrollTop] = useState(0);
    const containerRef = useRef();

    const totalHeight = items.length * itemHeight;
    const startIndex = Math.floor(scrollTop / itemHeight);
    const endIndex = Math.min(
        startIndex + Math.ceil(windowHeight / itemHeight) + 1,
        items.length
    );

    const visibleItems = items.slice(startIndex, endIndex);
    const offsetY = startIndex * itemHeight;

    const handleScroll = (e) => {
        setScrollTop(e.target.scrollTop);
    };

    return (
        <div
            ref={containerRef}
            onScroll={handleScroll}
            style={{
                height: windowHeight,
                overflow: 'auto',
                position: 'relative'
            }}
        >
            <div style={{ height: totalHeight }}>
                <div
                    style={{
                        position: 'absolute',
                        top: offsetY,
                        width: '100%'
                    }}
                >
                    {visibleItems.map((item) => (
                        <div
                            key={item.id}
                            style={{
                                height: itemHeight,
                                borderBottom: '1px solid #eee',
                                padding: '10px'
                            }}
                        >
                            {item.name}
                        </div>
                    ))}
                </div>
            </div>
        </div>
    );
}
tsx
import { useRef, useState, UIEvent } from 'react';

interface Item {
    id: number;
    name: string;
}

interface VirtualizedListProps {
    items: Item[];
    itemHeight?: number;
    windowHeight?: number;
}

function VirtualizedList({
    items,
    itemHeight = 50,
    windowHeight = 400
}: VirtualizedListProps): JSX.Element {
    const [scrollTop, setScrollTop] = useState<number>(0);
    const containerRef = useRef<HTMLDivElement>(null);

    const totalHeight: number = items.length * itemHeight;
    const startIndex: number = Math.floor(scrollTop / itemHeight);
    const endIndex: number = Math.min(
        startIndex + Math.ceil(windowHeight / itemHeight) + 1,
        items.length
    );

    const visibleItems: Item[] = items.slice(startIndex, endIndex);
    const offsetY: number = startIndex * itemHeight;

    const handleScroll = (e: UIEvent<HTMLDivElement>): void => {
        setScrollTop(e.currentTarget.scrollTop);
    };

    return (
        <div
            ref={containerRef}
            onScroll={handleScroll}
            style={{
                height: windowHeight,
                overflow: 'auto',
                position: 'relative'
            }}
        >
            <div style={{ height: totalHeight }}>
                <div
                    style={{
                        position: 'absolute',
                        top: offsetY,
                        width: '100%'
                    }}
                >
                    {visibleItems.map((item: Item) => (
                        <div
                            key={item.id}
                            style={{
                                height: itemHeight,
                                borderBottom: '1px solid #eee',
                                padding: '10px'
                            }}
                        >
                            {item.name}
                        </div>
                    ))}
                </div>
            </div>
        </div>
    );
}

Code Splitting

React.lazy and Suspense

jsx
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

// Lazy load components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));

// Loading fallback component
function LoadingSpinner() {
    return (
        <div className="loading">
            <div className="spinner"></div>
            <p>Loading...</p>
        </div>
    );
}

// App with code splitting
function App() {
    return (
        <Suspense fallback={<LoadingSpinner />}>
            <Routes>
                <Route path="/dashboard" element={<Dashboard />} />
                <Route path="/settings" element={<Settings />} />
                <Route path="/analytics" element={<Analytics />} />
            </Routes>
        </Suspense>
    );
}
tsx
import { lazy, Suspense, LazyExoticComponent, ComponentType } from 'react';
import { Routes, Route } from 'react-router-dom';

// Lazy load components with type annotations
const Dashboard: LazyExoticComponent<ComponentType> = lazy(
    () => import('./pages/Dashboard')
);
const Settings: LazyExoticComponent<ComponentType> = lazy(
    () => import('./pages/Settings')
);
const Analytics: LazyExoticComponent<ComponentType> = lazy(
    () => import('./pages/Analytics')
);

// Loading fallback component
function LoadingSpinner(): JSX.Element {
    return (
        <div className="loading">
            <div className="spinner"></div>
            <p>Loading...</p>
        </div>
    );
}

// App with code splitting
function App(): JSX.Element {
    return (
        <Suspense fallback={<LoadingSpinner />}>
            <Routes>
                <Route path="/dashboard" element={<Dashboard />} />
                <Route path="/settings" element={<Settings />} />
                <Route path="/analytics" element={<Analytics />} />
            </Routes>
        </Suspense>
    );
}

Named Exports with Lazy

jsx
import { lazy } from 'react';

// For components that are not default exports
const Dashboard = lazy(() =>
    import('./pages/Dashboard').then(module => ({
        default: module.Dashboard
    }))
);

// Or create a utility
function lazyNamed(importFn, componentName) {
    return lazy(() =>
        importFn().then(module => ({
            default: module[componentName]
        }))
    );
}

const Settings = lazyNamed(
    () => import('./pages/Settings'),
    'Settings'
);
tsx
import { lazy, LazyExoticComponent, ComponentType } from 'react';

// For components that are not default exports
const Dashboard: LazyExoticComponent<ComponentType> = lazy(() =>
    import('./pages/Dashboard').then(module => ({
        default: module.Dashboard
    }))
);

// Or create a utility with generics
function lazyNamed<T extends ComponentType<any>>(
    importFn: () => Promise<{ [key: string]: T }>,
    componentName: string
): LazyExoticComponent<T> {
    return lazy(() =>
        importFn().then(module => ({
            default: module[componentName]
        }))
    );
}

const Settings = lazyNamed<ComponentType>(
    () => import('./pages/Settings'),
    'Settings'
);

Compound Components Pattern

jsx
import { createContext, useContext, useState } from 'react';

// Context for compound component
const AccordionContext = createContext();

// Main component
function Accordion({ children, defaultOpen = null }) {
    const [openItem, setOpenItem] = useState(defaultOpen);

    const toggle = (id) => {
        setOpenItem(prev => prev === id ? null : id);
    };

    return (
        <AccordionContext.Provider value={{ openItem, toggle }}>
            <div className="accordion">
                {children}
            </div>
        </AccordionContext.Provider>
    );
}

// Item component
function AccordionItem({ children, id }) {
    const { openItem } = useContext(AccordionContext);
    const isOpen = openItem === id;

    return (
        <div className={`accordion-item ${isOpen ? 'open' : ''}`}>
            {children}
        </div>
    );
}

// Header component
function AccordionHeader({ children, id }) {
    const { openItem, toggle } = useContext(AccordionContext);
    const isOpen = openItem === id;

    return (
        <button
            className="accordion-header"
            onClick={() => toggle(id)}
            aria-expanded={isOpen}
        >
            {children}
            <span className="icon">{isOpen ? '-' : '+'}</span>
        </button>
    );
}

// Panel component
function AccordionPanel({ children, id }) {
    const { openItem } = useContext(AccordionContext);
    const isOpen = openItem === id;

    if (!isOpen) return null;

    return (
        <div className="accordion-panel">
            {children}
        </div>
    );
}

// Attach sub-components
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Panel = AccordionPanel;

// Usage
function FAQ() {
    return (
        <Accordion defaultOpen="q1">
            <Accordion.Item id="q1">
                <Accordion.Header id="q1">
                    What is React?
                </Accordion.Header>
                <Accordion.Panel id="q1">
                    React is a JavaScript library for building user interfaces.
                </Accordion.Panel>
            </Accordion.Item>

            <Accordion.Item id="q2">
                <Accordion.Header id="q2">
                    Why use React?
                </Accordion.Header>
                <Accordion.Panel id="q2">
                    React makes it easy to build interactive UIs.
                </Accordion.Panel>
            </Accordion.Item>
        </Accordion>
    );
}
tsx
import { createContext, useContext, useState, ReactNode } from 'react';

// Type definitions
interface AccordionContextValue {
    openItem: string | null;
    toggle: (id: string) => void;
}

interface AccordionProps {
    children: ReactNode;
    defaultOpen?: string | null;
}

interface AccordionItemProps {
    children: ReactNode;
    id: string;
}

interface AccordionHeaderProps {
    children: ReactNode;
    id: string;
}

interface AccordionPanelProps {
    children: ReactNode;
    id: string;
}

// Context for compound component
const AccordionContext = createContext<AccordionContextValue | null>(null);

// Custom hook for context
function useAccordion(): AccordionContextValue {
    const context = useContext(AccordionContext);
    if (!context) {
        throw new Error('Accordion components must be used within Accordion');
    }
    return context;
}

// Main component
function Accordion({ children, defaultOpen = null }: AccordionProps): JSX.Element {
    const [openItem, setOpenItem] = useState<string | null>(defaultOpen);

    const toggle = (id: string): void => {
        setOpenItem((prev: string | null) => prev === id ? null : id);
    };

    return (
        <AccordionContext.Provider value={{ openItem, toggle }}>
            <div className="accordion">
                {children}
            </div>
        </AccordionContext.Provider>
    );
}

// Item component
function AccordionItem({ children, id }: AccordionItemProps): JSX.Element {
    const { openItem } = useAccordion();
    const isOpen: boolean = openItem === id;

    return (
        <div className={`accordion-item ${isOpen ? 'open' : ''}`}>
            {children}
        </div>
    );
}

// Header component
function AccordionHeader({ children, id }: AccordionHeaderProps): JSX.Element {
    const { openItem, toggle } = useAccordion();
    const isOpen: boolean = openItem === id;

    return (
        <button
            className="accordion-header"
            onClick={() => toggle(id)}
            aria-expanded={isOpen}
        >
            {children}
            <span className="icon">{isOpen ? '-' : '+'}</span>
        </button>
    );
}

// Panel component
function AccordionPanel({ children, id }: AccordionPanelProps): JSX.Element | null {
    const { openItem } = useAccordion();
    const isOpen: boolean = openItem === id;

    if (!isOpen) return null;

    return (
        <div className="accordion-panel">
            {children}
        </div>
    );
}

// Attach sub-components with proper typing
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Panel = AccordionPanel;

// Usage
function FAQ(): JSX.Element {
    return (
        <Accordion defaultOpen="q1">
            <Accordion.Item id="q1">
                <Accordion.Header id="q1">
                    What is React?
                </Accordion.Header>
                <Accordion.Panel id="q1">
                    React is a JavaScript library for building user interfaces.
                </Accordion.Panel>
            </Accordion.Item>

            <Accordion.Item id="q2">
                <Accordion.Header id="q2">
                    Why use React?
                </Accordion.Header>
                <Accordion.Panel id="q2">
                    React makes it easy to build interactive UIs.
                </Accordion.Panel>
            </Accordion.Item>
        </Accordion>
    );
}

Render Props Pattern

jsx
import { useState, useEffect } from 'react';

// Render prop component
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);
    }, []);

    return render(position);
}

// Usage with render prop
function App() {
    return (
        <MouseTracker
            render={({ x, y }) => (
                <div>
                    <h1>Mouse Position</h1>
                    <p>X: {x}, Y: {y}</p>
                </div>
            )}
        />
    );
}

// Alternative: children as function
function DataFetcher({ url, children }) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        fetch(url)
            .then(res => res.json())
            .then(data => {
                setData(data);
                setLoading(false);
            })
            .catch(err => {
                setError(err);
                setLoading(false);
            });
    }, [url]);

    return children({ data, loading, error });
}

// Usage
function UserList() {
    return (
        <DataFetcher url="/api/users">
            {({ data, loading, error }) => {
                if (loading) return <p>Loading...</p>;
                if (error) return <p>Error: {error.message}</p>;
                return (
                    <ul>
                        {data?.map(user => (
                            <li key={user.id}>{user.name}</li>
                        ))}
                    </ul>
                );
            }}
        </DataFetcher>
    );
}
tsx
import { useState, useEffect, ReactNode } from 'react';

// Type definitions
interface Position {
    x: number;
    y: number;
}

interface MouseTrackerProps {
    render: (position: Position) => ReactNode;
}

interface DataFetcherProps<T> {
    url: string;
    children: (state: {
        data: T | null;
        loading: boolean;
        error: Error | null;
    }) => ReactNode;
}

interface User {
    id: number;
    name: string;
}

// Render prop component
function MouseTracker({ render }: MouseTrackerProps): JSX.Element {
    const [position, setPosition] = useState<Position>({ x: 0, y: 0 });

    useEffect(() => {
        const handleMouseMove = (e: MouseEvent): void => {
            setPosition({ x: e.clientX, y: e.clientY });
        };

        window.addEventListener('mousemove', handleMouseMove);
        return () => window.removeEventListener('mousemove', handleMouseMove);
    }, []);

    return <>{render(position)}</>;
}

// Usage with render prop
function App(): JSX.Element {
    return (
        <MouseTracker
            render={({ x, y }: Position) => (
                <div>
                    <h1>Mouse Position</h1>
                    <p>X: {x}, Y: {y}</p>
                </div>
            )}
        />
    );
}

// Generic DataFetcher: children as function
function DataFetcher<T>({ url, children }: DataFetcherProps<T>): JSX.Element {
    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState<boolean>(true);
    const [error, setError] = useState<Error | null>(null);

    useEffect(() => {
        fetch(url)
            .then((res: Response) => res.json())
            .then((data: T) => {
                setData(data);
                setLoading(false);
            })
            .catch((err: Error) => {
                setError(err);
                setLoading(false);
            });
    }, [url]);

    return <>{children({ data, loading, error })}</>;
}

// Usage
function UserList(): JSX.Element {
    return (
        <DataFetcher<User[]> url="/api/users">
            {({ data, loading, error }) => {
                if (loading) return <p>Loading...</p>;
                if (error) return <p>Error: {error.message}</p>;
                return (
                    <ul>
                        {data?.map((user: User) => (
                            <li key={user.id}>{user.name}</li>
                        ))}
                    </ul>
                );
            }}
        </DataFetcher>
    );
}

Higher-Order Components (HOC)

jsx
import { Navigate } from 'react-router-dom';

// HOC for adding loading state
function withLoading(WrappedComponent) {
    return function WithLoadingComponent({ isLoading, ...props }) {
        if (isLoading) {
            return <div className="loading">Loading...</div>;
        }
        return <WrappedComponent {...props} />;
    };
}

// HOC for authentication
function withAuth(WrappedComponent) {
    return function WithAuthComponent(props) {
        const { isAuthenticated, user } = useAuth();

        if (!isAuthenticated) {
            return <Navigate to="/login" />;
        }

        return <WrappedComponent {...props} user={user} />;
    };
}

// Usage
const UserListWithLoading = withLoading(UserList);
const ProtectedDashboard = withAuth(Dashboard);
tsx
import { ComponentType } from 'react';
import { Navigate } from 'react-router-dom';

// Type definitions
interface User {
    id: string;
    name: string;
}

interface WithLoadingProps {
    isLoading: boolean;
}

interface WithAuthProps {
    user: User;
}

// HOC for adding loading state
function withLoading<P extends object>(
    WrappedComponent: ComponentType<P>
): ComponentType<P & WithLoadingProps> {
    return function WithLoadingComponent({
        isLoading,
        ...props
    }: P & WithLoadingProps): JSX.Element {
        if (isLoading) {
            return <div className="loading">Loading...</div>;
        }
        return <WrappedComponent {...(props as P)} />;
    };
}

// HOC for authentication
function withAuth<P extends object>(
    WrappedComponent: ComponentType<P & WithAuthProps>
): ComponentType<Omit<P, keyof WithAuthProps>> {
    return function WithAuthComponent(
        props: Omit<P, keyof WithAuthProps>
    ): JSX.Element {
        const { isAuthenticated, user } = useAuth();

        if (!isAuthenticated) {
            return <Navigate to="/login" />;
        }

        return <WrappedComponent {...(props as P)} user={user} />;
    };
}

// Usage
interface UserListProps {
    title: string;
}

interface DashboardProps {
    user: User;
}

const UserListWithLoading = withLoading<UserListProps>(UserList);
const ProtectedDashboard = withAuth<DashboardProps>(Dashboard);

Error Boundaries

jsx
import { Component } from 'react';

class ErrorBoundary extends Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false, error: null };
    }

    static getDerivedStateFromError(error) {
        return { hasError: true, error };
    }

    componentDidCatch(error, errorInfo) {
        // Log to error reporting service
        console.error('Error:', error);
        console.error('Error Info:', errorInfo);
    }

    render() {
        if (this.state.hasError) {
            return (
                this.props.fallback || (
                    <div className="error-boundary">
                        <h2>Something went wrong</h2>
                        <button onClick={() => this.setState({ hasError: false })}>
                            Try again
                        </button>
                    </div>
                )
            );
        }

        return this.props.children;
    }
}

// Usage
function App() {
    return (
        <ErrorBoundary fallback={<ErrorPage />}>
            <Header />
            <ErrorBoundary fallback={<p>Widget failed to load</p>}>
                <ComplexWidget />
            </ErrorBoundary>
            <Footer />
        </ErrorBoundary>
    );
}
tsx
import { Component, ReactNode, ErrorInfo } from 'react';

// Type definitions
interface ErrorBoundaryProps {
    children: ReactNode;
    fallback?: ReactNode;
}

interface ErrorBoundaryState {
    hasError: boolean;
    error: Error | null;
}

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
    constructor(props: ErrorBoundaryProps) {
        super(props);
        this.state = { hasError: false, error: null };
    }

    static getDerivedStateFromError(error: Error): ErrorBoundaryState {
        return { hasError: true, error };
    }

    componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
        // Log to error reporting service
        console.error('Error:', error);
        console.error('Error Info:', errorInfo);
    }

    render(): ReactNode {
        if (this.state.hasError) {
            return (
                this.props.fallback || (
                    <div className="error-boundary">
                        <h2>Something went wrong</h2>
                        <button onClick={() => this.setState({ hasError: false, error: null })}>
                            Try again
                        </button>
                    </div>
                )
            );
        }

        return this.props.children;
    }
}

// Usage
function App(): JSX.Element {
    return (
        <ErrorBoundary fallback={<ErrorPage />}>
            <Header />
            <ErrorBoundary fallback={<p>Widget failed to load</p>}>
                <ComplexWidget />
            </ErrorBoundary>
            <Footer />
        </ErrorBoundary>
    );
}

Custom Hook with useReducer

jsx
import { useReducer, useCallback } from 'react';

function useShoppingCart() {
    const initialState = {
        items: [],
        total: 0,
        itemCount: 0
    };

    function cartReducer(state, action) {
        switch (action.type) {
            case 'ADD_ITEM': {
                const existingItem = state.items.find(
                    item => item.id === action.payload.id
                );

                let newItems;
                if (existingItem) {
                    newItems = state.items.map(item =>
                        item.id === action.payload.id
                            ? { ...item, quantity: item.quantity + 1 }
                            : item
                    );
                } else {
                    newItems = [...state.items, { ...action.payload, quantity: 1 }];
                }

                return {
                    ...state,
                    items: newItems,
                    total: newItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
                    itemCount: newItems.reduce((sum, item) => sum + item.quantity, 0)
                };
            }

            case 'REMOVE_ITEM': {
                const newItems = state.items.filter(
                    item => item.id !== action.payload
                );
                return {
                    ...state,
                    items: newItems,
                    total: newItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
                    itemCount: newItems.reduce((sum, item) => sum + item.quantity, 0)
                };
            }

            case 'CLEAR_CART':
                return initialState;

            default:
                return state;
        }
    }

    const [state, dispatch] = useReducer(cartReducer, initialState);

    const addItem = useCallback((item) => {
        dispatch({ type: 'ADD_ITEM', payload: item });
    }, []);

    const removeItem = useCallback((id) => {
        dispatch({ type: 'REMOVE_ITEM', payload: id });
    }, []);

    const clearCart = useCallback(() => {
        dispatch({ type: 'CLEAR_CART' });
    }, []);

    return {
        ...state,
        addItem,
        removeItem,
        clearCart
    };
}
tsx
import { useReducer, useCallback } from 'react';

// Type definitions
interface CartItem {
    id: number;
    name: string;
    price: number;
    quantity: number;
}

interface CartState {
    items: CartItem[];
    total: number;
    itemCount: number;
}

type CartAction =
    | { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
    | { type: 'REMOVE_ITEM'; payload: number }
    | { type: 'CLEAR_CART' };

interface UseShoppingCartResult extends CartState {
    addItem: (item: Omit<CartItem, 'quantity'>) => void;
    removeItem: (id: number) => void;
    clearCart: () => void;
}

function useShoppingCart(): UseShoppingCartResult {
    const initialState: CartState = {
        items: [],
        total: 0,
        itemCount: 0
    };

    function cartReducer(state: CartState, action: CartAction): CartState {
        switch (action.type) {
            case 'ADD_ITEM': {
                const existingItem = state.items.find(
                    (item: CartItem) => item.id === action.payload.id
                );

                let newItems: CartItem[];
                if (existingItem) {
                    newItems = state.items.map((item: CartItem) =>
                        item.id === action.payload.id
                            ? { ...item, quantity: item.quantity + 1 }
                            : item
                    );
                } else {
                    newItems = [...state.items, { ...action.payload, quantity: 1 }];
                }

                return {
                    ...state,
                    items: newItems,
                    total: newItems.reduce(
                        (sum: number, item: CartItem) => sum + item.price * item.quantity,
                        0
                    ),
                    itemCount: newItems.reduce(
                        (sum: number, item: CartItem) => sum + item.quantity,
                        0
                    )
                };
            }

            case 'REMOVE_ITEM': {
                const newItems = state.items.filter(
                    (item: CartItem) => item.id !== action.payload
                );
                return {
                    ...state,
                    items: newItems,
                    total: newItems.reduce(
                        (sum: number, item: CartItem) => sum + item.price * item.quantity,
                        0
                    ),
                    itemCount: newItems.reduce(
                        (sum: number, item: CartItem) => sum + item.quantity,
                        0
                    )
                };
            }

            case 'CLEAR_CART':
                return initialState;

            default:
                return state;
        }
    }

    const [state, dispatch] = useReducer(cartReducer, initialState);

    const addItem = useCallback((item: Omit<CartItem, 'quantity'>): void => {
        dispatch({ type: 'ADD_ITEM', payload: item });
    }, []);

    const removeItem = useCallback((id: number): void => {
        dispatch({ type: 'REMOVE_ITEM', payload: id });
    }, []);

    const clearCart = useCallback((): void => {
        dispatch({ type: 'CLEAR_CART' });
    }, []);

    return {
        ...state,
        addItem,
        removeItem,
        clearCart
    };
}

Portal for Modals

jsx
import { createPortal } from 'react-dom';
import { useEffect, useState } from 'react';

function Modal({ isOpen, onClose, children }) {
    const [mounted, setMounted] = useState(false);

    useEffect(() => {
        setMounted(true);

        if (isOpen) {
            document.body.style.overflow = 'hidden';
        }

        return () => {
            document.body.style.overflow = '';
        };
    }, [isOpen]);

    if (!mounted || !isOpen) return null;

    return createPortal(
        <div className="modal-overlay" onClick={onClose}>
            <div
                className="modal-content"
                onClick={e => e.stopPropagation()}
            >
                <button className="modal-close" onClick={onClose}>
                    x
                </button>
                {children}
            </div>
        </div>,
        document.body
    );
}

// Usage
function App() {
    const [showModal, setShowModal] = useState(false);

    return (
        <div>
            <button onClick={() => setShowModal(true)}>
                Open Modal
            </button>

            <Modal isOpen={showModal} onClose={() => setShowModal(false)}>
                <h2>Modal Title</h2>
                <p>Modal content goes here.</p>
            </Modal>
        </div>
    );
}
tsx
import { createPortal } from 'react-dom';
import { useEffect, useState, ReactNode, MouseEvent } from 'react';

interface ModalProps {
    isOpen: boolean;
    onClose: () => void;
    children: ReactNode;
}

function Modal({ isOpen, onClose, children }: ModalProps): JSX.Element | null {
    const [mounted, setMounted] = useState<boolean>(false);

    useEffect(() => {
        setMounted(true);

        if (isOpen) {
            document.body.style.overflow = 'hidden';
        }

        return () => {
            document.body.style.overflow = '';
        };
    }, [isOpen]);

    if (!mounted || !isOpen) return null;

    return createPortal(
        <div className="modal-overlay" onClick={onClose}>
            <div
                className="modal-content"
                onClick={(e: MouseEvent<HTMLDivElement>) => e.stopPropagation()}
            >
                <button className="modal-close" onClick={onClose}>
                    x
                </button>
                {children}
            </div>
        </div>,
        document.body
    );
}

// Usage
function App(): JSX.Element {
    const [showModal, setShowModal] = useState<boolean>(false);

    return (
        <div>
            <button onClick={() => setShowModal(true)}>
                Open Modal
            </button>

            <Modal isOpen={showModal} onClose={() => setShowModal(false)}>
                <h2>Modal Title</h2>
                <p>Modal content goes here.</p>
            </Modal>
        </div>
    );
}

Summary

PatternUse CaseTypeScript Benefit
Context APIGlobal state, theming, authType-safe context values
React.memoPrevent re-renders of pure componentsTyped comparison functions
useMemo/useCallbackMemoize values and callbacksReturn type inference
Code SplittingReduce initial bundle sizeTyped lazy imports
Compound ComponentsRelated components that share stateTyped context and props
Render PropsShare logic between componentsGeneric render functions
HOCsCross-cutting concernsGeneric component wrapping
Error BoundariesGraceful error handlingTyped error states
PortalsRender outside DOM hierarchySame type safety

Congratulations!

You've completed the React tutorial series! You now have a solid foundation in:

  • JSX and component basics (JavaScript and TypeScript)
  • State management with hooks
  • Event handling and forms
  • Routing and navigation
  • Data fetching patterns
  • Advanced optimization techniques

Keep practicing and building projects to solidify your knowledge!


Back to React Overview →