React Component Patterns and Design Patterns

1. Higher-Order Components (HOC) Pattern

HOC Pattern Syntax Description Use Case
Basic HOC withFeature(Component) Function that takes component and returns enhanced component Add props, wrap with context, inject dependencies
HOC with Arguments withFeature(config)(Comp) Curried HOC that accepts configuration Configurable behavior, conditional enhancement
Props Proxy props => <Comp {...props}/> HOC manipulates props before passing to wrapped component Transform, filter, or add props
Inheritance Inversion class extends WrappedComp HOC extends wrapped component class DEPRECATED Modify lifecycle, render - avoid, use hooks instead
Composing HOCs compose(hoc1, hoc2)(Comp) Apply multiple HOCs in sequence Combine multiple enhancements cleanly
Display Name HOC.displayName = 'with...' Set display name for debugging Better DevTools, error messages, stack traces

Example: Basic HOC with authentication

// Basic HOC pattern
function withAuth(Component) {
  return function AuthComponent(props) {
    const { user, loading } = useAuth();
    
    if (loading) return <LoadingSpinner />;
    if (!user) return <Navigate to="/login" />;
    
    return <Component {...props} user={user} />;
  };
}

// Usage
const ProtectedDashboard = withAuth(Dashboard);

// HOC with configuration
function withPermission(permission) {
  return function (Component) {
    return function PermissionComponent(props) {
      const { user } = useAuth();
      
      if (!user?.permissions.includes(permission)) {
        return <AccessDenied />;
      }
      
      return <Component {...props} />;
    };
  };
}

// Usage with config
const AdminPanel = withPermission('admin')(Panel);

Example: Props transformation and composition

// Props transformation HOC
function withUppercase(Component) {
  return function UppercaseComponent({ name, ...props }) {
    return <Component {...props} name={name?.toUpperCase()} />;
  };
}

// Data fetching HOC
function withData(fetchFn, propName) {
  return function (Component) {
    return function DataComponent(props) {
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(true);
      
      useEffect(() => {
        fetchFn().then((result) => {
          setData(result);
          setLoading(false);
        });
      }, []);
      
      return <Component {...props} {...{[propName]: data}} loading={loading} />;
    };
  };
}

// Compose multiple HOCs
import { compose } from 'redux'; // or lodash/fp

const enhance = compose(
  withAuth,
  withData(fetchUserProfile, 'profile'),
  withUppercase
);

const EnhancedProfile = enhance(ProfileComponent);
HOC Best Practices: Use descriptive names (with* prefix), pass all props through, set displayName for debugging, prefer hooks for most cases, compose HOCs with utility functions, avoid mutating wrapped component.

2. Render Props and Function-as-Children Pattern

Pattern Syntax Description Use Case
Render Prop <Comp render={data => ...}/> Pass function as prop that returns React element Share logic while giving render control to consumer
Children as Function <Comp>{data => ...}</Comp> Children prop is a function receiving data More ergonomic API, follows React conventions
Multiple Render Props header={...} footer={...} Multiple function props for different render areas Flexible layout with pluggable sections
Render Props with Hooks useData().render() Hooks replaced most render prop use cases PREFERRED Modern alternative - cleaner, more composable

Example: Mouse tracker with render prop

// Render prop component
function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  useEffect(() => {
    const handleMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);
  
  return render(position);
}

// Usage with render prop
<MouseTracker 
  render={({ x, y }) => (
    <h1>Mouse at {x}, {y}</h1>
  )}
/>

// Children as function pattern
function DataProvider({ url, children }) {
  const [data, loading, error] = useFetch(url);
  return children({ data, loading, error });
}

// Usage
<DataProvider url="/api/users">
  {({ data, loading, error }) => {
    if (loading) return <Spinner />;
    if (error) return <Error message={error} />;
    return <UserList users={data} />;
  }}
</DataProvider>

Example: Modern hook alternative (preferred)

// Custom hook replaces render prop pattern
function useMouse() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  useEffect(() => {
    const handleMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);
  
  return position;
}

// Usage - much cleaner!
function MyComponent() {
  const { x, y } = useMouse();
  return <h1>Mouse at {x}, {y}</h1>;
}
Warning: Render props can hurt performance if function is recreated on each render. Use useCallback for render prop functions. Consider custom hooks as modern alternative.

3. Compound Components Pattern and Component APIs

Pattern Syntax Description Use Case
Compound Components Menu.Item, Menu.Divider Components designed to work together, share implicit state Complex UI with coordinated subcomponents
Static Properties Component.SubComponent Attach child components as static properties of parent Namespace related components, cleaner imports
Context-based Context.Provider + useContext Share state via context between parent and children Flexible structure, works at any nesting level
Clone Element React.cloneElement(child) Clone and inject props into children Add props to unknown children, less flexible
Controlled Compound activeItem={...} onChange Parent controls shared state externally Integrate with forms, external state management

Example: Tabs compound component with context

// Context for shared state
const TabContext = createContext();

function Tabs({ children, defaultTab }) {
  const [activeTab, setActiveTab] = useState(defaultTab);
  
  return (
    <TabContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabContext.Provider>
  );
}

function TabList({ children }) {
  return <div className="tab-list" role="tablist">{children}</div>;
}

function Tab({ id, children }) {
  const { activeTab, setActiveTab } = useContext(TabContext);
  const isActive = activeTab === id;
  
  return (
    <button
      role="tab"
      aria-selected={isActive}
      onClick={() => setActiveTab(id)}
      className={isActive ? 'active' : ''}
    >
      {children}
    </button>
  );
}

function TabPanel({ id, children }) {
  const { activeTab } = useContext(TabContext);
  if (activeTab !== id) return null;
  
  return <div role="tabpanel">{children}</div>;
}

// Attach as static properties
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

// Usage - declarative API
<Tabs defaultTab="home">
  <Tabs.List>
    <Tabs.Tab id="home">Home</Tabs.Tab>
    <Tabs.Tab id="profile">Profile</Tabs.Tab>
    <Tabs.Tab id="settings">Settings</Tabs.Tab>
  </Tabs.List>
  
  <Tabs.Panel id="home">Home content</Tabs.Panel>
  <Tabs.Panel id="profile">Profile content</Tabs.Panel>
  <Tabs.Panel id="settings">Settings content</Tabs.Panel>
</Tabs>

Example: Controlled compound component

// Controlled version for external state control
function ControlledTabs({ children, activeTab, onTabChange }) {
  return (
    <TabContext.Provider value={{ activeTab, setActiveTab: onTabChange }}>
      <div className="tabs">{children}</div>
    </TabContext.Provider>
  );
}

// Usage with external state
function App() {
  const [tab, setTab] = useState('home');
  
  return (
    <>
      <button onClick={() => setTab('profile')}>
        Go to Profile (external)
      </button>
      
      <ControlledTabs activeTab={tab} onTabChange={setTab}>
        <Tabs.List>
          <Tabs.Tab id="home">Home</Tabs.Tab>
          <Tabs.Tab id="profile">Profile</Tabs.Tab>
        </Tabs.List>
        <Tabs.Panel id="home">Home</Tabs.Panel>
        <Tabs.Panel id="profile">Profile</Tabs.Panel>
      </ControlledTabs>
    </>
  );
}
Compound Component Benefits: Flexible, declarative API; implicit state sharing; easy to extend; semantic JSX structure; reduced prop drilling; better separation of concerns.

4. Custom Hooks Pattern for Logic Reuse

Hook Pattern Purpose Returns Use Case
State Hook Encapsulate stateful logic [state, setState] Toggle, counter, form state, modal open/close
Effect Hook Side effects and subscriptions void or cleanup Event listeners, timers, subscriptions, polling
Data Hook Data fetching and caching { data, loading, error } API calls, async data, pagination, infinite scroll
Computation Hook Derived state and calculations computedValue Filtering, sorting, validation, formatting
Ref Hook DOM access, stable refs ref Focus management, measurements, previous values
Composite Hook Combine multiple hooks { ...multiple values } Complex workflows, coordinated state

Example: Custom hooks for common patterns

// Toggle hook
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  
  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);
  
  return [value, toggle, setTrue, setFalse];
}

// Usage
const [isOpen, toggle, open, close] = useToggle();

// Local storage hook
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });
  
  const setValue = useCallback((value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error('Error saving to localStorage:', error);
    }
  }, [key, storedValue]);
  
  return [storedValue, setValue];
}

// Previous value hook
function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

Example: Data fetching and debounce hooks

// Fetch hook with loading/error states
function useFetch(url, options) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    let isCancelled = false;
    
    setLoading(true);
    fetch(url, options)
      .then(res => res.json())
      .then(data => {
        if (!isCancelled) {
          setData(data);
          setError(null);
        }
      })
      .catch(err => {
        if (!isCancelled) {
          setError(err.message);
          setData(null);
        }
      })
      .finally(() => {
        if (!isCancelled) setLoading(false);
      });
    
    return () => { isCancelled = true; };
  }, [url, JSON.stringify(options)]);
  
  return { data, loading, error };
}

// Debounce hook
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => clearTimeout(handler);
  }, [value, delay]);
  
  return debouncedValue;
}

// Usage - search with debounce
function SearchComponent() {
  const [search, setSearch] = useState('');
  const debouncedSearch = useDebounce(search, 500);
  const { data, loading } = useFetch(`/api/search?q=${debouncedSearch}`);
  
  return (
    <div>
      <input value={search} onChange={e => setSearch(e.target.value)} />
      {loading ? <Spinner /> : <Results data={data} />}
    </div>
  );
}
Custom Hook Guidelines: Always start with 'use' prefix, follow hooks rules, return values/functions consumers need, use useCallback for returned functions, document parameters and return values, handle cleanup in useEffect.

5. Polymorphic Components and Generic TypeScript Props

Pattern Implementation Description Use Case
As Prop Pattern <Box as="button"> Component renders as different HTML elements Flexible primitive components, design systems
Component Prop <Comp component={Link}> Accept component as prop to customize rendering Wrapper components, list items, custom elements
Render Function renderItem={(item) => ...} Function prop that returns rendered content Lists, grids, custom rendering logic
TypeScript Generic <T extends ElementType> Type-safe polymorphic components with TS TS Fully typed props based on element type

Example: Polymorphic Box component

// Basic polymorphic component
function Box({ as: Component = 'div', children, ...props }) {
  return <Component {...props}>{children}</Component>;
}

// Usage - renders as different elements
<Box>Default div</Box>
<Box as="section">Section element</Box>
<Box as="button" onClick={handleClick}>Button element</Box>
<Box as={Link} to="/home">React Router Link</Box>

// TypeScript polymorphic component with full type safety
import { ElementType, ComponentPropsWithoutRef } from 'react';

type PolymorphicProps<T extends ElementType> = {
  as?: T;
  children?: React.ReactNode;
} & ComponentPropsWithoutRef<T>;

function PolymorphicBox<T extends ElementType = 'div'>({
  as,
  children,
  ...props
}: PolymorphicProps<T>) {
  const Component = as || 'div';
  return <Component {...props}>{children}</Component>;
}

// TypeScript enforces correct props based on 'as' prop
<PolymorphicBox as="a" href="/path">Link</PolymorphicBox> // ✓ href allowed
<PolymorphicBox as="button" onClick={fn}>Button</PolymorphicBox> // ✓ onClick allowed
<PolymorphicBox as="div" href="/path">Div</PolymorphicBox> // ✗ TS error - href not valid

Example: Generic list component

// Generic list with custom rendering
function List({ items, renderItem, emptyMessage = 'No items' }) {
  if (!items?.length) {
    return <div className="empty">{emptyMessage}</div>;
  }
  
  return (
    <ul className="list">
      {items.map((item, index) => (
        <li key={item.id || index}>
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
}

// Usage with different render functions
<List 
  items={users} 
  renderItem={(user) => <UserCard user={user} />}
/>

<List 
  items={products} 
  renderItem={(product, idx) => (
    <div>
      {idx + 1}. {product.name} - ${product.price}
    </div>
  )}
/>

// With component prop pattern
function Container({ component: Component = 'div', children, ...props }) {
  return (
    <Component className="container" {...props}>
      {children}
    </Component>
  );
}

// Usage
<Container>Default div container</Container>
<Container component="section">Section container</Container>
<Container component={Card}>Custom component</Container>
Polymorphic Component Benefits: Single component, multiple elements; reduces component proliferation; type-safe with TypeScript; flexible for design systems; maintains consistent styling/behavior.

6. Component Composition vs Inheritance - React Philosophy

Approach React Recommendation Pattern Reason
Composition PREFERRED Strongly recommended Children prop, props, slots More flexible, easier to understand, React way
Inheritance AVOID Not recommended Class extends Component Tight coupling, less flexible, not idiomatic React
Containment Use children prop <Box>{children}</Box> Generic containers, wrappers, layouts
Specialization Compose with props <Dialog type="alert"> Specific variants from generic components
Named Slots Multiple props header={...} footer={...} Complex layouts with multiple sections

Example: Composition patterns (preferred)

// Containment - generic container with children
function Dialog({ title, children, footer }) {
  return (
    <div className="dialog">
      <div className="dialog-header">
        <h2>{title}</h2>
      </div>
      <div className="dialog-body">
        {children}
      </div>
      {footer && (
        <div className="dialog-footer">
          {footer}
        </div>
      )}
    </div>
  );
}

// Specialization - specific dialog variants via composition
function WelcomeDialog() {
  return (
    <Dialog 
      title="Welcome"
      footer={
        <button onClick={handleStart}>Get Started</button>
      }
    >
      <p>Thank you for joining!</p>
    </Dialog>
  );
}

function ConfirmDialog({ onConfirm, onCancel }) {
  return (
    <Dialog 
      title="Confirm Action"
      footer={
        <>
          <button onClick={onCancel}>Cancel</button>
          <button onClick={onConfirm}>Confirm</button>
        </>
      }
    >
      <p>Are you sure you want to proceed?</p>
    </Dialog>
  );
}

// Named slots pattern
function Layout({ header, sidebar, main, footer }) {
  return (
    <div className="layout">
      <header>{header}</header>
      <div className="layout-body">
        <aside>{sidebar}</aside>
        <main>{main}</main>
      </div>
      <footer>{footer}</footer>
    </div>
  );
}

// Usage
<Layout
  header={<Header />}
  sidebar={<Sidebar />}
  main={<MainContent />}
  footer={<Footer />}
/>

Example: Why composition beats inheritance

// ❌ Inheritance approach (avoid)
class BaseDialog extends Component {
  render() {
    return (
      <div className="dialog">
        <h2>{this.props.title}</h2>
        {this.renderContent()}
        {this.renderActions()}
      </div>
    );
  }
}

class WelcomeDialog extends BaseDialog {
  renderContent() {
    return <p>Welcome!</p>;
  }
  renderActions() {
    return <button>Get Started</button>;
  }
}
// Issues: Tight coupling, hard to modify, fragile base class

// ✓ Composition approach (preferred)
function Dialog({ title, children, actions }) {
  return (
    <div className="dialog">
      <h2>{title}</h2>
      <div>{children}</div>
      <div>{actions}</div>
    </div>
  );
}

function WelcomeDialog() {
  return (
    <Dialog 
      title="Welcome"
      actions={<button>Get Started</button>}
    >
      <p>Welcome!</p>
    </Dialog>
  );
}
// Benefits: Flexible, clear, easy to modify, testable

// Composition with hooks for behavior reuse
function useDialogState() {
  const [isOpen, setIsOpen] = useState(false);
  return { isOpen, open: () => setIsOpen(true), close: () => setIsOpen(false) };
}

function MyComponent() {
  const dialog = useDialogState();
  
  return (
    <>
      <button onClick={dialog.open}>Open Dialog</button>
      {dialog.isOpen && (
        <Dialog title="My Dialog" onClose={dialog.close}>
          Content here
        </Dialog>
      )}
    </>
  );
}
Warning: React does NOT recommend using inheritance for component reuse. Props and composition give you all the flexibility you need. Use hooks for behavior reuse, not class inheritance.

Advanced Component Patterns Best Practices