Skip to content
Home / Skills / React / Component Patterns
RE

Component Patterns

React core v1.0.0

React Component Patterns

Overview

React component patterns establish reusable, maintainable, and testable UI building blocks. This skill covers composition patterns, compound components, render props, hooks, and higher-order components.


Key Concepts

Component Architecture

┌─────────────────────────────────────────────────────────────┐
│                  React Component Hierarchy                   │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                    App (Root)                        │   │
│  │  • Global state providers                            │   │
│  │  • Theme, Auth, Router context                       │   │
│  └─────────────────────────────────────────────────────┘   │
│                         │                                    │
│    ┌────────────────────┼────────────────────┐              │
│    ▼                    ▼                    ▼              │
│  ┌────────┐       ┌──────────┐        ┌──────────┐         │
│  │ Layout │       │  Pages   │        │  Shared  │         │
│  │        │       │          │        │Components│         │
│  │Header  │       │Dashboard │        │  Button  │         │
│  │Sidebar │       │ Profile  │        │  Modal   │         │
│  │Footer  │       │ Settings │        │  Form    │         │
│  └────────┘       └──────────┘        └──────────┘         │
│                         │                                    │
│             ┌───────────┴───────────┐                       │
│             ▼                       ▼                       │
│       ┌──────────┐           ┌──────────┐                  │
│       │ Features │           │ Domain   │                  │
│       │          │           │Components│                  │
│       │UserTable │           │OrderCard │                  │
│       │OrderList │           │CartItem  │                  │
│       └──────────┘           └──────────┘                  │
│                                                              │
│  Component Types:                                           │
│  ┌────────────────────────────────────────────────────┐    │
│  │ Presentational: UI only, receive data via props    │    │
│  │ Container: Business logic, data fetching           │    │
│  │ Layout: Page structure, positioning                │    │
│  │ Feature: Complete functionality, domain-specific   │    │
│  │ Utility: Higher-order, providers, error boundaries │    │
│  └────────────────────────────────────────────────────┘    │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Best Practices

1. Prefer Composition Over Inheritance

Use props.children and slots for flexible components.

2. Keep Components Small and Focused

Single responsibility - one component, one job.

3. Lift State Only When Necessary

Start with local state, lift when sharing is needed.

4. Use TypeScript for Props

Define explicit interfaces for all component props.

5. Separate Concerns with Custom Hooks

Extract reusable logic into custom hooks.


Code Examples

Example 1: Compound Components

import React, { createContext, useContext, useState, ReactNode } from 'react';

// Context for compound component state
interface AccordionContextValue {
  activeItems: Set<string>;
  toggle: (id: string) => void;
  allowMultiple: boolean;
}

const AccordionContext = createContext<AccordionContextValue | null>(null);

function useAccordionContext() {
  const context = useContext(AccordionContext);
  if (!context) {
    throw new Error('Accordion components must be used within Accordion');
  }
  return context;
}

// Root component
interface AccordionProps {
  children: ReactNode;
  allowMultiple?: boolean;
  defaultActive?: string[];
}

function Accordion({ children, allowMultiple = false, defaultActive = [] }: AccordionProps) {
  const [activeItems, setActiveItems] = useState<Set<string>>(
    new Set(defaultActive)
  );

  const toggle = (id: string) => {
    setActiveItems((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        if (!allowMultiple) {
          next.clear();
        }
        next.add(id);
      }
      return next;
    });
  };

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

// Item component
interface AccordionItemProps {
  id: string;
  children: ReactNode;
}

function AccordionItem({ id, children }: AccordionItemProps) {
  const { activeItems } = useAccordionContext();
  const isActive = activeItems.has(id);

  return (
    <div className={`accordion-item ${isActive ? 'active' : ''}`} data-id={id}>
      {children}
    </div>
  );
}

// Header component
interface AccordionHeaderProps {
  id: string;
  children: ReactNode;
}

function AccordionHeader({ id, children }: AccordionHeaderProps) {
  const { activeItems, toggle } = useAccordionContext();
  const isActive = activeItems.has(id);

  return (
    <button
      className="accordion-header"
      onClick={() => toggle(id)}
      aria-expanded={isActive}
      aria-controls={`panel-${id}`}
      role="tab"
    >
      {children}
      <span className="accordion-icon">{isActive ? '−' : '+'}</span>
    </button>
  );
}

// Panel component
interface AccordionPanelProps {
  id: string;
  children: ReactNode;
}

function AccordionPanel({ id, children }: AccordionPanelProps) {
  const { activeItems } = useAccordionContext();
  const isActive = activeItems.has(id);

  if (!isActive) return null;

  return (
    <div
      className="accordion-panel"
      id={`panel-${id}`}
      role="tabpanel"
      aria-hidden={!isActive}
    >
      {children}
    </div>
  );
}

// Attach subcomponents
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Panel = AccordionPanel;

// Usage
function FAQSection() {
  return (
    <Accordion allowMultiple defaultActive={['faq-1']}>
      <Accordion.Item id="faq-1">
        <Accordion.Header id="faq-1">What is React?</Accordion.Header>
        <Accordion.Panel id="faq-1">
          React is a JavaScript library for building user interfaces.
        </Accordion.Panel>
      </Accordion.Item>
      <Accordion.Item id="faq-2">
        <Accordion.Header id="faq-2">What are hooks?</Accordion.Header>
        <Accordion.Panel id="faq-2">
          Hooks let you use state and other React features without classes.
        </Accordion.Panel>
      </Accordion.Item>
    </Accordion>
  );
}

Example 2: Render Props Pattern

import React, { useState, useCallback, ReactNode } from 'react';

// Render props for mouse position
interface MousePosition {
  x: number;
  y: number;
}

interface MouseTrackerProps {
  children: (position: MousePosition) => ReactNode;
}

function MouseTracker({ children }: MouseTrackerProps) {
  const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });

  const handleMouseMove = useCallback((event: React.MouseEvent) => {
    setPosition({
      x: event.clientX,
      y: event.clientY,
    });
  }, []);

  return (
    <div onMouseMove={handleMouseMove} style={{ height: '100%' }}>
      {children(position)}
    </div>
  );
}

// Render props for async data fetching
interface AsyncDataProps<T> {
  url: string;
  children: (state: {
    data: T | null;
    loading: boolean;
    error: Error | null;
    refetch: () => void;
  }) => ReactNode;
}

function AsyncData<T>({ url, children }: AsyncDataProps<T>) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error: ${response.status}`);
      }
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err : new Error('Unknown error'));
    } finally {
      setLoading(false);
    }
  }, [url]);

  React.useEffect(() => {
    fetchData();
  }, [fetchData]);

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

// Usage
function UserProfile({ userId }: { userId: string }) {
  return (
    <AsyncData<User> url={`/api/users/${userId}`}>
      {({ data: user, loading, error, refetch }) => {
        if (loading) return <Spinner />;
        if (error) return <ErrorMessage error={error} retry={refetch} />;
        if (!user) return null;

        return (
          <div className="user-profile">
            <h1>{user.name}</h1>
            <p>{user.email}</p>
          </div>
        );
      }}
    </AsyncData>
  );
}

Example 3: Custom Hooks

import { useState, useEffect, useCallback, useRef } from 'react';

// Debounced value hook
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// Local storage hook with sync
function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  // Get initial value from storage or use default
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  // Sync with localStorage
  const setValue = useCallback(
    (value: T | ((prev: T) => T)) => {
      setStoredValue((prev) => {
        const valueToStore = value instanceof Function ? value(prev) : value;
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
        return valueToStore;
      });
    },
    [key]
  );

  // Listen for changes from other tabs
  useEffect(() => {
    const handleStorageChange = (e: StorageEvent) => {
      if (e.key === key && e.newValue) {
        setStoredValue(JSON.parse(e.newValue));
      }
    };

    window.addEventListener('storage', handleStorageChange);
    return () => window.removeEventListener('storage', handleStorageChange);
  }, [key]);

  return [storedValue, setValue];
}

// Async hook with loading and error states
interface UseAsyncState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

interface UseAsyncReturn<T> extends UseAsyncState<T> {
  execute: () => Promise<void>;
  reset: () => void;
}

function useAsync<T>(
  asyncFunction: () => Promise<T>,
  immediate = true
): UseAsyncReturn<T> {
  const [state, setState] = useState<UseAsyncState<T>>({
    data: null,
    loading: immediate,
    error: null,
  });

  const execute = useCallback(async () => {
    setState((prev) => ({ ...prev, loading: true, error: null }));
    
    try {
      const data = await asyncFunction();
      setState({ data, loading: false, error: null });
    } catch (error) {
      setState({
        data: null,
        loading: false,
        error: error instanceof Error ? error : new Error('Unknown error'),
      });
    }
  }, [asyncFunction]);

  const reset = useCallback(() => {
    setState({ data: null, loading: false, error: null });
  }, []);

  useEffect(() => {
    if (immediate) {
      execute();
    }
  }, [execute, immediate]);

  return { ...state, execute, reset };
}

// Usage
function SearchComponent() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);
  const [preferences, setPreferences] = useLocalStorage('search-prefs', {
    sortBy: 'relevance',
    limit: 10,
  });

  const searchFn = useCallback(
    () => searchAPI(debouncedQuery, preferences),
    [debouncedQuery, preferences]
  );

  const { data: results, loading, error, execute: search } = useAsync(
    searchFn,
    false
  );

  useEffect(() => {
    if (debouncedQuery) {
      search();
    }
  }, [debouncedQuery, search]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {loading && <Spinner />}
      {error && <ErrorMessage error={error} />}
      {results && <ResultsList results={results} />}
    </div>
  );
}

Example 4: Higher-Order Components

import React, { ComponentType, useEffect } from 'react';

// HOC for authentication
interface WithAuthProps {
  user: User;
}

function withAuth<P extends WithAuthProps>(
  WrappedComponent: ComponentType<P>
): ComponentType<Omit<P, keyof WithAuthProps>> {
  return function WithAuthComponent(props: Omit<P, keyof WithAuthProps>) {
    const { user, loading } = useAuth();
    const navigate = useNavigate();

    useEffect(() => {
      if (!loading && !user) {
        navigate('/login');
      }
    }, [user, loading, navigate]);

    if (loading) {
      return <LoadingScreen />;
    }

    if (!user) {
      return null;
    }

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

// HOC for error boundary
interface WithErrorBoundaryOptions {
  fallback?: React.ReactNode;
  onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}

function withErrorBoundary<P>(
  WrappedComponent: ComponentType<P>,
  options: WithErrorBoundaryOptions = {}
): ComponentType<P> {
  return class ErrorBoundary extends React.Component<P, { hasError: boolean }> {
    state = { hasError: false };

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

    componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
      options.onError?.(error, errorInfo);
    }

    render() {
      if (this.state.hasError) {
        return options.fallback || <div>Something went wrong</div>;
      }

      return <WrappedComponent {...this.props} />;
    }
  };
}

// HOC for feature flags
function withFeatureFlag<P>(
  WrappedComponent: ComponentType<P>,
  flagName: string,
  fallback: React.ReactNode = null
): ComponentType<P> {
  return function WithFeatureFlag(props: P) {
    const { isEnabled } = useFeatureFlag(flagName);

    if (!isEnabled) {
      return <>{fallback}</>;
    }

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

// Usage - compose HOCs
const ProtectedDashboard = withAuth(
  withErrorBoundary(
    withFeatureFlag(Dashboard, 'new-dashboard'),
    { fallback: <ErrorFallback /> }
  )
);

Example 5: Polymorphic Components

import React, { ElementType, ComponentPropsWithoutRef, ReactNode } from 'react';

// Polymorphic component type
type PolymorphicProps<E extends ElementType, Props = {}> = Props & {
  as?: E;
  children?: ReactNode;
} & Omit<ComponentPropsWithoutRef<E>, keyof Props | 'as' | 'children'>;

// Button that can render as different elements
interface ButtonBaseProps {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  loading?: boolean;
}

type ButtonProps<E extends ElementType = 'button'> = PolymorphicProps<E, ButtonBaseProps>;

function Button<E extends ElementType = 'button'>({
  as,
  variant = 'primary',
  size = 'md',
  loading = false,
  children,
  className,
  disabled,
  ...props
}: ButtonProps<E>) {
  const Component = as || 'button';

  const classNames = [
    'btn',
    `btn-${variant}`,
    `btn-${size}`,
    loading && 'btn-loading',
    className,
  ]
    .filter(Boolean)
    .join(' ');

  return (
    <Component
      className={classNames}
      disabled={disabled || loading}
      {...props}
    >
      {loading && <Spinner size="sm" />}
      {children}
    </Component>
  );
}

// Usage
function Navigation() {
  return (
    <nav>
      {/* Renders as button */}
      <Button onClick={handleClick}>Click me</Button>

      {/* Renders as anchor */}
      <Button as="a" href="/dashboard">
        Go to Dashboard
      </Button>

      {/* Renders as React Router Link */}
      <Button as={Link} to="/profile" variant="secondary">
        Profile
      </Button>
    </nav>
  );
}

// Text component with polymorphism
interface TextBaseProps {
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
  weight?: 'normal' | 'medium' | 'semibold' | 'bold';
  color?: 'primary' | 'secondary' | 'muted' | 'error';
}

type TextProps<E extends ElementType = 'span'> = PolymorphicProps<E, TextBaseProps>;

function Text<E extends ElementType = 'span'>({
  as,
  size = 'md',
  weight = 'normal',
  color = 'primary',
  children,
  className,
  ...props
}: TextProps<E>) {
  const Component = as || 'span';

  return (
    <Component
      className={`text text-${size} text-${weight} text-${color} ${className || ''}`}
      {...props}
    >
      {children}
    </Component>
  );
}

// Usage
function Article() {
  return (
    <article>
      <Text as="h1" size="xl" weight="bold">
        Article Title
      </Text>
      <Text as="p" color="muted">
        Published on January 1, 2025
      </Text>
      <Text as="div" size="lg">
        Article content...
      </Text>
    </article>
  );
}

Anti-Patterns

❌ Prop Drilling

// WRONG - passing props through many levels
function App() {
  const [user, setUser] = useState<User | null>(null);
  return <Layout user={user} setUser={setUser} />;
}
function Layout({ user, setUser }) {
  return <Header user={user} setUser={setUser} />;
}

// ✅ CORRECT - use context
const UserContext = createContext<UserContextValue | null>(null);

function App() {
  const [user, setUser] = useState<User | null>(null);
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <Layout />
    </UserContext.Provider>
  );
}

❌ Massive Components

Split large components into smaller, focused ones with single responsibilities.


References