Advanced React Patterns: Compound Components, Render Props, and Custom Hooks

React, Design Patterns|SEPTEMBER 10, 2025|0 VIEWS
Master advanced React patterns to build flexible, reusable, and maintainable components

Introduction

As React applications grow in complexity, developers need sophisticated patterns to create flexible, reusable, and maintainable components. This comprehensive guide explores three powerful React patterns: Compound Components, Render Props, and Custom Hooks. These patterns enable you to build component APIs that are both powerful and intuitive, leading to better developer experience and more maintainable codebases.

Understanding Advanced React Patterns

Why Advanced Patterns Matter

Advanced patterns solve common problems in React development:

  • Flexibility: Components that can adapt to different use cases
  • Reusability: Code that can be shared across different parts of an application
  • Maintainability: Clean, understandable component APIs
  • Separation of Concerns: Clear boundaries between logic and presentation

Pattern Selection Guide

// When to use each pattern:

// Compound Components: When building UI components with multiple related parts
<Modal>
  <Modal.Header>Title</Modal.Header>
  <Modal.Body>Content</Modal.Body>
  <Modal.Footer>Actions</Modal.Footer>
</Modal>

// Render Props: When sharing stateful logic between components
<DataFetcher url="/api/users">
  {({ data, loading, error }) => (
    loading ? <Spinner /> : <UserList users={data} />
  )}
</DataFetcher>

// Custom Hooks: When extracting and sharing stateful logic
const { data, loading, error } = useApiData('/api/users');

1. Compound Components Pattern

The Compound Components pattern allows you to create components that work together to form a complete UI while maintaining flexibility in their arrangement and usage.

Basic Compound Component

// components/Modal/Modal.jsx
import React, { createContext, useContext, useState } from 'react';
import './Modal.css';

// Create context for sharing state between compound components
const ModalContext = createContext();

// Custom hook to access modal context
const useModalContext = () => {
  const context = useContext(ModalContext);
  if (!context) {
    throw new Error('Modal compound components must be used within Modal');
  }
  return context;
};

// Main Modal component
const Modal = ({ children, isOpen: controlledIsOpen, onClose }) => {
  const [internalIsOpen, setInternalIsOpen] = useState(false);

  // Support both controlled and uncontrolled modes
  const isOpen =
    controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen;
  const setIsOpen =
    controlledIsOpen !== undefined ? onClose : setInternalIsOpen;

  const close = () => setIsOpen(false);
  const open = () => setIsOpen(true);

  const contextValue = {
    isOpen,
    close,
    open,
  };

  return (
    <ModalContext.Provider value={contextValue}>
      {children}
      {isOpen && (
        <div className="modal-overlay" onClick={close}>
          <div className="modal-container" onClick={(e) => e.stopPropagation()}>
            <div className="modal-content">{children}</div>
          </div>
        </div>
      )}
    </ModalContext.Provider>
  );
};

// Modal Header compound component
Modal.Header = ({ children, className = '' }) => {
  const { close } = useModalContext();

  return (
    <div className={`modal-header ${className}`}>
      <div className="modal-title">{children}</div>
      <button className="modal-close-button" onClick={close} aria-label="Close">
        ×
      </button>
    </div>
  );
};

// Modal Body compound component
Modal.Body = ({ children, className = '' }) => {
  return <div className={`modal-body ${className}`}>{children}</div>;
};

// Modal Footer compound component
Modal.Footer = ({ children, className = '' }) => {
  return <div className={`modal-footer ${className}`}>{children}</div>;
};

// Modal Trigger compound component
Modal.Trigger = ({ children, asChild = false }) => {
  const { open } = useModalContext();

  if (asChild) {
    return React.cloneElement(children, {
      onClick: open,
    });
  }

  return (
    <button onClick={open} className="modal-trigger">
      {children}
    </button>
  );
};

export default Modal;

Advanced Compound Component with Flexible API

// components/Tabs/Tabs.jsx
import React, {
  createContext,
  useContext,
  useState,
  useRef,
  useEffect,
} from 'react';

const TabsContext = createContext();

const useTabs = () => {
  const context = useContext(TabsContext);
  if (!context) {
    throw new Error('Tabs compounds must be used within Tabs');
  }
  return context;
};

const Tabs = ({
  children,
  defaultValue,
  value: controlledValue,
  onValueChange,
  orientation = 'horizontal',
  activationMode = 'automatic',
}) => {
  const [internalValue, setInternalValue] = useState(defaultValue);
  const isControlled = controlledValue !== undefined;
  const value = isControlled ? controlledValue : internalValue;

  const setValue = (newValue) => {
    if (!isControlled) {
      setInternalValue(newValue);
    }
    onValueChange?.(newValue);
  };

  const contextValue = {
    value,
    setValue,
    orientation,
    activationMode,
  };

  return (
    <TabsContext.Provider value={contextValue}>
      <div
        className={`tabs tabs--${orientation}`}
        data-orientation={orientation}
      >
        {children}
      </div>
    </TabsContext.Provider>
  );
};

// Tab List compound component
Tabs.List = ({ children, className = '' }) => {
  const { orientation } = useTabs();
  const listRef = useRef(null);

  const handleKeyDown = (event) => {
    const tabs = Array.from(listRef.current.querySelectorAll('[role="tab"]'));
    const currentIndex = tabs.findIndex((tab) => tab === event.target);

    let nextIndex;

    switch (event.key) {
      case 'ArrowRight':
      case 'ArrowDown':
        event.preventDefault();
        nextIndex = (currentIndex + 1) % tabs.length;
        break;
      case 'ArrowLeft':
      case 'ArrowUp':
        event.preventDefault();
        nextIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;
        break;
      case 'Home':
        event.preventDefault();
        nextIndex = 0;
        break;
      case 'End':
        event.preventDefault();
        nextIndex = tabs.length - 1;
        break;
      default:
        return;
    }

    tabs[nextIndex]?.focus();
  };

  return (
    <div
      ref={listRef}
      role="tablist"
      aria-orientation={orientation}
      className={`tabs-list ${className}`}
      onKeyDown={handleKeyDown}
    >
      {children}
    </div>
  );
};

// Tab Trigger compound component
Tabs.Trigger = ({
  children,
  value: tabValue,
  disabled = false,
  className = '',
}) => {
  const { value, setValue, activationMode } = useTabs();
  const isSelected = value === tabValue;

  const handleClick = () => {
    if (!disabled) {
      setValue(tabValue);
    }
  };

  const handleKeyDown = (event) => {
    if (
      activationMode === 'manual' &&
      (event.key === 'Enter' || event.key === ' ')
    ) {
      event.preventDefault();
      handleClick();
    }
  };

  const handleFocus = () => {
    if (activationMode === 'automatic' && !disabled) {
      setValue(tabValue);
    }
  };

  return (
    <button
      role="tab"
      aria-selected={isSelected}
      aria-controls={`panel-${tabValue}`}
      id={`tab-${tabValue}`}
      tabIndex={isSelected ? 0 : -1}
      disabled={disabled}
      className={`tabs-trigger ${
        isSelected ? 'tabs-trigger--active' : ''
      } ${className}`}
      onClick={handleClick}
      onKeyDown={handleKeyDown}
      onFocus={handleFocus}
    >
      {children}
    </button>
  );
};

// Tab Content compound component
Tabs.Content = ({ children, value: tabValue, className = '' }) => {
  const { value } = useTabs();
  const isSelected = value === tabValue;

  if (!isSelected) return null;

  return (
    <div
      role="tabpanel"
      aria-labelledby={`tab-${tabValue}`}
      id={`panel-${tabValue}`}
      tabIndex={0}
      className={`tabs-content ${className}`}
    >
      {children}
    </div>
  );
};

export default Tabs;

Usage Examples

// Usage: Basic Modal
function App() {
  return (
    <div>
      <Modal>
        <Modal.Trigger>Open Modal</Modal.Trigger>
        <Modal.Header>Confirmation</Modal.Header>
        <Modal.Body>
          <p>Are you sure you want to delete this item?</p>
        </Modal.Body>
        <Modal.Footer>
          <Modal.Trigger asChild>
            <button className="btn btn-secondary">Cancel</button>
          </Modal.Trigger>
          <button className="btn btn-danger">Delete</button>
        </Modal.Footer>
      </Modal>
    </div>
  );
}

// Usage: Advanced Tabs
function TabsExample() {
  const [activeTab, setActiveTab] = useState('tab1');

  return (
    <Tabs value={activeTab} onValueChange={setActiveTab}>
      <Tabs.List>
        <Tabs.Trigger value="tab1">Overview</Tabs.Trigger>
        <Tabs.Trigger value="tab2">Details</Tabs.Trigger>
        <Tabs.Trigger value="tab3" disabled>
          Settings
        </Tabs.Trigger>
      </Tabs.List>

      <Tabs.Content value="tab1">
        <h3>Overview Content</h3>
        <p>This is the overview panel.</p>
      </Tabs.Content>

      <Tabs.Content value="tab2">
        <h3>Details Content</h3>
        <p>This is the details panel.</p>
      </Tabs.Content>

      <Tabs.Content value="tab3">
        <h3>Settings Content</h3>
        <p>This is the settings panel.</p>
      </Tabs.Content>
    </Tabs>
  );
}

2. Render Props Pattern

The Render Props pattern is a technique for sharing code between React components using a prop whose value is a function.

Basic Render Props Implementation

// components/DataFetcher/DataFetcher.jsx
import React, { useState, useEffect } from 'react';

const DataFetcher = ({
  url,
  children,
  render,
  transformData = (data) => data,
  dependencies = [],
}) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const result = await response.json();

        if (!cancelled) {
          setData(transformData(result));
          setLoading(false);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      cancelled = true;
    };
  }, [url, ...dependencies]);

  const refetch = () => {
    setData(null);
    setError(null);
    setLoading(true);
    // Trigger re-fetch by updating a dependency
  };

  const renderProps = {
    data,
    loading,
    error,
    refetch,
  };

  // Support both children as function and render prop
  if (typeof children === 'function') {
    return children(renderProps);
  }

  if (typeof render === 'function') {
    return render(renderProps);
  }

  return null;
};

export default DataFetcher;

Advanced Render Props with Multiple Data Sources

// components/MultiDataProvider/MultiDataProvider.jsx
import React, { useState, useEffect, useCallback } from 'react';

const MultiDataProvider = ({ sources, children, onError }) => {
  const [dataState, setDataState] = useState({});
  const [loadingState, setLoadingState] = useState({});
  const [errorState, setErrorState] = useState({});

  const fetchData = useCallback(
    async (key, config) => {
      setLoadingState((prev) => ({ ...prev, [key]: true }));
      setErrorState((prev) => ({ ...prev, [key]: null }));

      try {
        const { url, transform = (data) => data, ...options } = config;
        const response = await fetch(url, options);

        if (!response.ok) {
          throw new Error(`Failed to fetch ${key}: ${response.status}`);
        }

        const result = await response.json();
        const transformedData = transform(result);

        setDataState((prev) => ({ ...prev, [key]: transformedData }));
      } catch (error) {
        setErrorState((prev) => ({ ...prev, [key]: error.message }));
        onError?.(key, error);
      } finally {
        setLoadingState((prev) => ({ ...prev, [key]: false }));
      }
    },
    [onError]
  );

  useEffect(() => {
    Object.entries(sources).forEach(([key, config]) => {
      fetchData(key, config);
    });
  }, [sources, fetchData]);

  const refetch = (key) => {
    if (key && sources[key]) {
      fetchData(key, sources[key]);
    } else {
      // Refetch all
      Object.entries(sources).forEach(([sourceKey, config]) => {
        fetchData(sourceKey, config);
      });
    }
  };

  const isLoading = Object.values(loadingState).some(Boolean);
  const hasError = Object.values(errorState).some(Boolean);
  const allLoaded = Object.keys(sources).every(
    (key) => dataState[key] !== undefined || errorState[key]
  );

  return children({
    data: dataState,
    loading: loadingState,
    error: errorState,
    isLoading,
    hasError,
    allLoaded,
    refetch,
  });
};

export default MultiDataProvider;

Mouse Tracker Render Props Example

// components/MouseTracker/MouseTracker.jsx
import React, { useState, useEffect, useRef } from 'react';

const MouseTracker = ({
  children,
  trackOutside = false,
  throttle = 16, // ~60fps
}) => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [isInside, setIsInside] = useState(false);
  const containerRef = useRef(null);
  const throttleRef = useRef(null);

  useEffect(() => {
    const updatePosition = (clientX, clientY) => {
      if (throttleRef.current) return;

      throttleRef.current = setTimeout(() => {
        throttleRef.current = null;
      }, throttle);

      if (containerRef.current) {
        const rect = containerRef.current.getBoundingClientRect();
        setPosition({
          x: clientX - rect.left,
          y: clientY - rect.top,
          clientX,
          clientY,
          relativeX: (clientX - rect.left) / rect.width,
          relativeY: (clientY - rect.top) / rect.height,
        });
      }
    };

    const handleMouseMove = (event) => {
      updatePosition(event.clientX, event.clientY);
    };

    const handleMouseEnter = () => setIsInside(true);
    const handleMouseLeave = () => setIsInside(false);

    const element = containerRef.current;
    if (element) {
      element.addEventListener('mousemove', handleMouseMove);
      element.addEventListener('mouseenter', handleMouseEnter);
      element.addEventListener('mouseleave', handleMouseLeave);

      if (trackOutside) {
        document.addEventListener('mousemove', handleMouseMove);
      }
    }

    return () => {
      if (element) {
        element.removeEventListener('mousemove', handleMouseMove);
        element.removeEventListener('mouseenter', handleMouseEnter);
        element.removeEventListener('mouseleave', handleMouseLeave);
      }

      if (trackOutside) {
        document.removeEventListener('mousemove', handleMouseMove);
      }

      if (throttleRef.current) {
        clearTimeout(throttleRef.current);
      }
    };
  }, [throttle, trackOutside]);

  return (
    <div ref={containerRef} style={{ height: '100%', width: '100%' }}>
      {children({ position, isInside })}
    </div>
  );
};

export default MouseTracker;

Usage Examples

// Usage: Data Fetching
function UserList() {
  return (
    <DataFetcher
      url="/api/users"
      transformData={(users) => users.filter((user) => user.active)}
    >
      {({ data, loading, error, refetch }) => {
        if (loading) return <div className="spinner">Loading...</div>;
        if (error)
          return (
            <div className="error">
              Error: {error}
              <button onClick={refetch}>Retry</button>
            </div>
          );

        return (
          <div>
            <button onClick={refetch}>Refresh</button>
            <ul>
              {data?.map((user) => (
                <li key={user.id}>{user.name}</li>
              ))}
            </ul>
          </div>
        );
      }}
    </DataFetcher>
  );
}

// Usage: Multiple Data Sources
function Dashboard() {
  return (
    <MultiDataProvider
      sources={{
        users: { url: '/api/users' },
        posts: {
          url: '/api/posts',
          transform: (posts) => posts.slice(0, 10),
        },
        stats: { url: '/api/stats' },
      }}
    >
      {({ data, loading, error, refetch }) => (
        <div>
          <h1>Dashboard</h1>

          {loading.users && <p>Loading users...</p>}
          {data.users && <UserStats users={data.users} />}

          {loading.posts && <p>Loading posts...</p>}
          {data.posts && <RecentPosts posts={data.posts} />}

          {loading.stats && <p>Loading statistics...</p>}
          {data.stats && <Statistics stats={data.stats} />}

          <button onClick={() => refetch()}>Refresh All</button>
        </div>
      )}
    </MultiDataProvider>
  );
}

// Usage: Mouse Tracking
function InteractiveBox() {
  return (
    <MouseTracker>
      {({ position, isInside }) => (
        <div
          style={{
            width: 300,
            height: 200,
            border: '2px solid #ccc',
            borderColor: isInside ? '#007bff' : '#ccc',
            position: 'relative',
            backgroundColor: isInside ? '#f8f9fa' : 'white',
          }}
        >
          <h3>Mouse Tracker</h3>
          <p>
            X: {Math.round(position.x)}, Y: {Math.round(position.y)}
          </p>
          <p>
            Relative: {(position.relativeX * 100).toFixed(1)}%,{' '}
            {(position.relativeY * 100).toFixed(1)}%
          </p>

          {isInside && (
            <div
              style={{
                position: 'absolute',
                left: position.x - 5,
                top: position.y - 5,
                width: 10,
                height: 10,
                backgroundColor: 'red',
                borderRadius: '50%',
                pointerEvents: 'none',
              }}
            />
          )}
        </div>
      )}
    </MouseTracker>
  );
}

3. Custom Hooks Pattern

Custom Hooks allow you to extract component logic into reusable functions, promoting code reuse and separation of concerns.

Basic Custom Hooks

// hooks/useToggle.js
import { useState, useCallback } from 'react';

const useToggle = (initialValue = false) => {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => setValue((prev) => !prev), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);

  return [value, { toggle, setTrue, setFalse, setValue }];
};

export default useToggle;
// hooks/useLocalStorage.js
import { useState, useEffect, useCallback } from 'react';

const useLocalStorage = (key, initialValue) => {
  // Get initial value from localStorage or use provided initial value
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  // Update localStorage when state changes
  const setValue = useCallback(
    (value) => {
      try {
        // Allow value to be a function so we have the same API as useState
        const valueToStore =
          value instanceof Function ? value(storedValue) : value;
        setStoredValue(valueToStore);
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      } catch (error) {
        console.error(`Error setting localStorage key "${key}":`, error);
      }
    },
    [key, storedValue]
  );

  // Remove item from localStorage
  const removeValue = useCallback(() => {
    try {
      window.localStorage.removeItem(key);
      setStoredValue(initialValue);
    } catch (error) {
      console.error(`Error removing localStorage key "${key}":`, error);
    }
  }, [key, initialValue]);

  return [storedValue, setValue, removeValue];
};

export default useLocalStorage;

Advanced Data Fetching Hook

// hooks/useApiData.js
import { useState, useEffect, useCallback, useRef } from 'react';

const useApiData = (url, options = {}) => {
  const {
    immediate = true,
    transform = (data) => data,
    onSuccess,
    onError,
    dependencies = [],
    retryAttempts = 3,
    retryDelay = 1000,
  } = options;

  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(immediate);
  const [error, setError] = useState(null);
  const [retryCount, setRetryCount] = useState(0);

  const abortControllerRef = useRef(null);
  const retryTimeoutRef = useRef(null);

  const fetchData = useCallback(
    async (retryAttempt = 0) => {
      try {
        setLoading(true);
        setError(null);

        // Cancel previous request
        if (abortControllerRef.current) {
          abortControllerRef.current.abort();
        }

        abortControllerRef.current = new AbortController();

        const response = await fetch(url, {
          signal: abortControllerRef.current.signal,
          ...options.fetchOptions,
        });

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const result = await response.json();
        const transformedData = transform(result);

        setData(transformedData);
        setRetryCount(0);
        onSuccess?.(transformedData);
      } catch (err) {
        if (err.name === 'AbortError') {
          return; // Request was cancelled
        }

        console.error('Fetch error:', err);

        if (retryAttempt < retryAttempts) {
          setRetryCount(retryAttempt + 1);
          retryTimeoutRef.current = setTimeout(() => {
            fetchData(retryAttempt + 1);
          }, retryDelay * Math.pow(2, retryAttempt)); // Exponential backoff
        } else {
          setError(err.message);
          onError?.(err);
        }
      } finally {
        setLoading(false);
      }
    },
    [
      url,
      transform,
      onSuccess,
      onError,
      retryAttempts,
      retryDelay,
      options.fetchOptions,
    ]
  );

  const refetch = useCallback(() => {
    setRetryCount(0);
    fetchData();
  }, [fetchData]);

  const cancel = useCallback(() => {
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }
    if (retryTimeoutRef.current) {
      clearTimeout(retryTimeoutRef.current);
    }
    setLoading(false);
  }, []);

  useEffect(() => {
    if (immediate && url) {
      fetchData();
    }

    return () => {
      cancel();
    };
  }, [url, immediate, fetchData, cancel, ...dependencies]);

  return {
    data,
    loading,
    error,
    refetch,
    cancel,
    retryCount,
  };
};

export default useApiData;

Advanced Form Management Hook

// hooks/useForm.js
import { useState, useCallback, useRef } from 'react';

const useForm = (initialValues = {}, options = {}) => {
  const {
    validate,
    onSubmit,
    validateOnChange = false,
    validateOnBlur = true,
  } = options;

  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submitCount, setSubmitCount] = useState(0);

  const formRef = useRef(null);

  const validateField = useCallback(
    (name, value) => {
      if (!validate) return null;

      const fieldErrors = validate({ ...values, [name]: value });
      return fieldErrors[name] || null;
    },
    [validate, values]
  );

  const validateForm = useCallback(
    (formValues = values) => {
      if (!validate) return {};
      return validate(formValues) || {};
    },
    [validate, values]
  );

  const setFieldValue = useCallback(
    (name, value) => {
      setValues((prev) => ({ ...prev, [name]: value }));

      if (validateOnChange) {
        const fieldError = validateField(name, value);
        setErrors((prev) => ({
          ...prev,
          [name]: fieldError,
        }));
      }
    },
    [validateOnChange, validateField]
  );

  const setFieldError = useCallback((name, error) => {
    setErrors((prev) => ({ ...prev, [name]: error }));
  }, []);

  const setFieldTouched = useCallback((name, isTouched = true) => {
    setTouched((prev) => ({ ...prev, [name]: isTouched }));
  }, []);

  const handleChange = useCallback(
    (event) => {
      const { name, value, type, checked } = event.target;
      const fieldValue = type === 'checkbox' ? checked : value;
      setFieldValue(name, fieldValue);
    },
    [setFieldValue]
  );

  const handleBlur = useCallback(
    (event) => {
      const { name } = event.target;
      setFieldTouched(name, true);

      if (validateOnBlur) {
        const fieldError = validateField(name, values[name]);
        setErrors((prev) => ({ ...prev, [name]: fieldError }));
      }
    },
    [setFieldTouched, validateOnBlur, validateField, values]
  );

  const resetForm = useCallback(
    (newValues = initialValues) => {
      setValues(newValues);
      setErrors({});
      setTouched({});
      setIsSubmitting(false);
      setSubmitCount(0);
    },
    [initialValues]
  );

  const handleSubmit = useCallback(
    async (event) => {
      event.preventDefault();
      setSubmitCount((prev) => prev + 1);

      const formErrors = validateForm();
      setErrors(formErrors);

      // Mark all fields as touched
      const allTouched = Object.keys(values).reduce((acc, key) => {
        acc[key] = true;
        return acc;
      }, {});
      setTouched(allTouched);

      const hasErrors = Object.keys(formErrors).length > 0;

      if (!hasErrors && onSubmit) {
        setIsSubmitting(true);
        try {
          await onSubmit(values, {
            setFieldError,
            setFieldValue,
            resetForm,
            setErrors,
          });
        } catch (error) {
          console.error('Form submission error:', error);
        } finally {
          setIsSubmitting(false);
        }
      }
    },
    [values, validateForm, onSubmit, setFieldError, setFieldValue, resetForm]
  );

  const getFieldProps = useCallback(
    (name) => ({
      name,
      value: values[name] || '',
      onChange: handleChange,
      onBlur: handleBlur,
    }),
    [values, handleChange, handleBlur]
  );

  const getFieldMeta = useCallback(
    (name) => ({
      value: values[name],
      error: errors[name],
      touched: touched[name],
      invalid: Boolean(errors[name]),
      valid: !errors[name] && touched[name],
    }),
    [values, errors, touched]
  );

  const isValid = Object.keys(errors).length === 0;
  const isDirty = JSON.stringify(values) !== JSON.stringify(initialValues);

  return {
    values,
    errors,
    touched,
    isSubmitting,
    submitCount,
    isValid,
    isDirty,
    setFieldValue,
    setFieldError,
    setFieldTouched,
    setValues,
    setErrors,
    resetForm,
    handleSubmit,
    handleChange,
    handleBlur,
    getFieldProps,
    getFieldMeta,
    validateForm,
    formRef,
  };
};

export default useForm;

Intersection Observer Hook

// hooks/useIntersectionObserver.js
import { useState, useEffect, useRef } from 'react';

const useIntersectionObserver = (options = {}) => {
  const {
    threshold = 0.1,
    root = null,
    rootMargin = '0%',
    triggerOnce = false,
  } = options;

  const [isIntersecting, setIsIntersecting] = useState(false);
  const [hasIntersected, setHasIntersected] = useState(false);
  const elementRef = useRef(null);

  useEffect(() => {
    const element = elementRef.current;
    if (!element) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        const isElementIntersecting = entry.isIntersecting;

        if (isElementIntersecting) {
          setHasIntersected(true);
        }

        if (!triggerOnce || !hasIntersected) {
          setIsIntersecting(isElementIntersecting);
        }
      },
      { threshold, root, rootMargin }
    );

    observer.observe(element);

    return () => {
      observer.unobserve(element);
    };
  }, [threshold, root, rootMargin, triggerOnce, hasIntersected]);

  return [elementRef, isIntersecting, hasIntersected];
};

export default useIntersectionObserver;

Usage Examples

// Usage: Custom Hooks in Components
function TodoApp() {
  const [todos, setTodos] = useLocalStorage('todos', []);
  const [isModalOpen, { toggle: toggleModal, setFalse: closeModal }] =
    useToggle();

  const { values, errors, handleSubmit, getFieldProps, resetForm } = useForm(
    { title: '', description: '' },
    {
      validate: (values) => {
        const errors = {};
        if (!values.title) errors.title = 'Title is required';
        if (values.title && values.title.length < 3) {
          errors.title = 'Title must be at least 3 characters';
        }
        return errors;
      },
      onSubmit: async (formValues, { resetForm }) => {
        const newTodo = {
          id: Date.now(),
          ...formValues,
          completed: false,
        };
        setTodos((prev) => [...prev, newTodo]);
        resetForm();
        closeModal();
      },
    }
  );

  const { data: suggestions, loading } = useApiData('/api/suggestions', {
    dependencies: [values.title],
    immediate: values.title.length > 2,
    transform: (data) => data.slice(0, 5),
  });

  return (
    <div>
      <h1>Todo App</h1>

      <button onClick={toggleModal}>Add Todo</button>

      {isModalOpen && (
        <div className="modal">
          <form onSubmit={handleSubmit}>
            <input {...getFieldProps('title')} placeholder="Todo title" />
            {errors.title && <span className="error">{errors.title}</span>}

            <textarea
              {...getFieldProps('description')}
              placeholder="Description"
            />

            {loading && <div>Loading suggestions...</div>}
            {suggestions && suggestions.length > 0 && (
              <ul className="suggestions">
                {suggestions.map((suggestion) => (
                  <li key={suggestion.id}>{suggestion.title}</li>
                ))}
              </ul>
            )}

            <button type="submit">Add Todo</button>
            <button type="button" onClick={closeModal}>
              Cancel
            </button>
          </form>
        </div>
      )}

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <h3>{todo.title}</h3>
            <p>{todo.description}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

// Usage: Intersection Observer for Lazy Loading
function LazyImage({ src, alt, className }) {
  const [imgRef, isIntersecting] = useIntersectionObserver({
    triggerOnce: true,
    threshold: 0.1,
  });

  return (
    <div ref={imgRef} className={className}>
      {isIntersecting ? (
        <img src={src} alt={alt} />
      ) : (
        <div className="placeholder">Loading...</div>
      )}
    </div>
  );
}

Pattern Composition and Best Practices

Combining Patterns

Often, the most powerful solutions come from combining multiple patterns:

// components/DataTable/DataTable.jsx - Combining all three patterns
import React from 'react';
import useApiData from '../../hooks/useApiData';
import useLocalStorage from '../../hooks/useLocalStorage';
import usePagination from '../../hooks/usePagination';

// Compound component with custom hooks and render props
const DataTable = ({
  url,
  children,
  pageSize = 10,
  storageKey,
  transformData,
  onRowClick,
}) => {
  const [savedFilters, setSavedFilters] = useLocalStorage(
    `${storageKey}_filters`,
    {}
  );
  const [savedSort, setSavedSort] = useLocalStorage(`${storageKey}_sort`, {});

  const { data, loading, error, refetch } = useApiData(url, {
    transform: transformData,
    dependencies: [savedFilters, savedSort],
  });

  const {
    currentPage,
    totalPages,
    paginatedData,
    goToPage,
    nextPage,
    prevPage,
  } = usePagination(data || [], pageSize);

  const tableState = {
    data: paginatedData,
    loading,
    error,
    refetch,
    filters: savedFilters,
    setFilters: setSavedFilters,
    sort: savedSort,
    setSort: setSavedSort,
    pagination: {
      currentPage,
      totalPages,
      goToPage,
      nextPage,
      prevPage,
    },
    onRowClick,
  };

  return (
    <div className="data-table">
      {typeof children === 'function' ? children(tableState) : children}
    </div>
  );
};

// Compound components
DataTable.Header = ({ children, sortable, onSort, sortKey, currentSort }) => (
  <th
    className={sortable ? 'sortable' : ''}
    onClick={() => sortable && onSort(sortKey)}
  >
    {children}
    {sortable && currentSort.key === sortKey && (
      <span className={`sort-arrow ${currentSort.direction}`}>
        {currentSort.direction === 'asc' ? '↑' : '↓'}
      </span>
    )}
  </th>
);

DataTable.Row = ({ children, onClick, data }) => (
  <tr onClick={() => onClick?.(data)} className={onClick ? 'clickable' : ''}>
    {children}
  </tr>
);

DataTable.Cell = ({ children, className = '' }) => (
  <td className={className}>{children}</td>
);

DataTable.Pagination = ({ pagination }) => (
  <div className="pagination">
    <button
      onClick={pagination.prevPage}
      disabled={pagination.currentPage === 1}
    >
      Previous
    </button>

    <span>
      Page {pagination.currentPage} of {pagination.totalPages}
    </span>

    <button
      onClick={pagination.nextPage}
      disabled={pagination.currentPage === pagination.totalPages}
    >
      Next
    </button>
  </div>
);

// Usage
function UsersTable() {
  return (
    <DataTable
      url="/api/users"
      storageKey="users_table"
      transformData={(users) => users.filter((u) => u.active)}
      onRowClick={(user) => console.log('Clicked user:', user)}
    >
      {({ data, loading, error, sort, setSort, pagination, onRowClick }) => (
        <>
          {loading && <div>Loading...</div>}
          {error && <div>Error: {error}</div>}

          <table>
            <thead>
              <tr>
                <DataTable.Header
                  sortable
                  sortKey="name"
                  onSort={setSort}
                  currentSort={sort}
                >
                  Name
                </DataTable.Header>
                <DataTable.Header
                  sortable
                  sortKey="email"
                  onSort={setSort}
                  currentSort={sort}
                >
                  Email
                </DataTable.Header>
                <DataTable.Header>Actions</DataTable.Header>
              </tr>
            </thead>
            <tbody>
              {data?.map((user) => (
                <DataTable.Row key={user.id} data={user} onClick={onRowClick}>
                  <DataTable.Cell>{user.name}</DataTable.Cell>
                  <DataTable.Cell>{user.email}</DataTable.Cell>
                  <DataTable.Cell>
                    <button>Edit</button>
                    <button>Delete</button>
                  </DataTable.Cell>
                </DataTable.Row>
              ))}
            </tbody>
          </table>

          <DataTable.Pagination pagination={pagination} />
        </>
      )}
    </DataTable>
  );
}

Best Practices

1. When to Use Each Pattern

Compound Components:

  • ✅ Building UI component libraries
  • ✅ Components with multiple related parts
  • ✅ Need flexible component composition
  • ❌ Simple, single-purpose components
  • ❌ Non-UI logic sharing

Render Props:

  • ✅ Sharing stateful logic between components
  • ✅ Need maximum flexibility in rendering
  • ✅ Cross-cutting concerns (data fetching, subscriptions)
  • ❌ Simple, static components
  • ❌ Performance-critical rendering

Custom Hooks:

  • ✅ Extracting and reusing stateful logic
  • ✅ Side effects management
  • ✅ State management patterns
  • ✅ API integrations
  • ❌ UI component composition
  • ❌ Render logic sharing

2. Performance Considerations

// Optimizing render props with React.memo
const OptimizedDataFetcher = React.memo(
  ({ url, children }) => {
    // ... data fetching logic
    return children({ data, loading, error });
  },
  (prevProps, nextProps) => {
    return prevProps.url === nextProps.url;
  }
);

// Optimizing custom hooks with useMemo and useCallback
const useOptimizedApiData = (url, options) => {
  const memoizedOptions = useMemo(
    () => options,
    [
      options.immediate,
      options.retryAttempts,
      // ... other options
    ]
  );

  const fetchData = useCallback(async () => {
    // ... fetch logic
  }, [url, memoizedOptions]);

  // ... rest of the hook
};

// Optimizing compound components with context optimization
const ModalContext = createContext();

const ModalProvider = ({ children, value }) => {
  const memoizedValue = useMemo(
    () => value,
    [value.isOpen, value.close, value.open]
  );

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

3. TypeScript Integration

// Type-safe compound components
interface ModalContextType {
  isOpen: boolean;
  close: () => void;
  open: () => void;
}

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

interface ModalCompoundComponents {
  Header: React.FC<{ children: React.ReactNode; className?: string }>;
  Body: React.FC<{ children: React.ReactNode; className?: string }>;
  Footer: React.FC<{ children: React.ReactNode; className?: string }>;
  Trigger: React.FC<{ children: React.ReactNode; asChild?: boolean }>;
}

type ModalComponent = React.FC<ModalProps> & ModalCompoundComponents;

// Type-safe custom hooks
interface UseApiDataOptions<T> {
  immediate?: boolean;
  transform?: (data: any) => T;
  onSuccess?: (data: T) => void;
  onError?: (error: Error) => void;
  dependencies?: any[];
}

interface UseApiDataReturn<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
  refetch: () => void;
  cancel: () => void;
}

function useApiData<T = any>(
  url: string,
  options: UseApiDataOptions<T> = {}
): UseApiDataReturn<T> {
  // ... implementation
}

// Type-safe render props
interface DataFetcherProps<T> {
  url: string;
  children: (props: UseApiDataReturn<T>) => React.ReactNode;
  render?: (props: UseApiDataReturn<T>) => React.ReactNode;
  transformData?: (data: any) => T;
}

function DataFetcher<T = any>({
  url,
  children,
  render,
  transformData,
}: DataFetcherProps<T>) {
  // ... implementation
}

Conclusion

Advanced React patterns are powerful tools for building maintainable, flexible, and reusable components. Each pattern solves specific problems:

  • Compound Components excel at creating flexible UI component APIs
  • Render Props provide maximum flexibility for sharing component logic
  • Custom Hooks are perfect for extracting and reusing stateful logic

Key Takeaways

  1. Choose the Right Pattern: Each pattern has its strengths and appropriate use cases
  2. Combine Patterns: The most powerful solutions often combine multiple patterns
  3. Performance Matters: Always consider the performance implications of your pattern choices
  4. TypeScript Integration: Use TypeScript to make your patterns type-safe and more maintainable
  5. Start Simple: Begin with simpler patterns and evolve to more complex ones as needed

Pattern Selection Guidelines

  • Use Custom Hooks for logic extraction and reuse
  • Use Compound Components for flexible UI component APIs
  • Use Render Props when you need maximum rendering flexibility
  • Combine patterns when building complex, feature-rich components

By mastering these patterns, you'll be able to build React applications that are not only functional but also maintainable, testable, and enjoyable to work with. Remember that patterns are tools to solve problems – choose the right tool for each situation, and don't over-engineer simple solutions.