Modern React Design Patterns: A Comprehensive Guide

Design patterns represent time-tested solutions to common problems in software development. Understanding these React Design Patterns is crucial for building maintainable, scalable, and performant applications in the React ecosystem. As the React library has evolved—particularly with the introduction of Hooks in React 16.8—so too have the patterns and best practices for React development.

This guide explores the most effective React Design Patterns in modern React development. Whether building a small application or a complex enterprise system, these patterns will help you write cleaner, more maintainable code while avoiding common pitfalls.

React’s popularity stems partly from its flexibility—it doesn’t force developers into rigid structures. However, this flexibility can be a double-edged sword. Without established patterns, React applications can quickly become unwieldy. The React Design Patterns outlined in this article provide structure and consistency while preserving React’s flexibility.

1. Component Patterns

Functional vs. Class Components

Modern React development favors functional components with Hooks over class components.

// Modern functional approach
   function UserProfile({ name, email }) {
    const [isEditing, setIsEditing] = useState(false);

    return (
     <div className="user-profile">
       <h2>{name}</h2>
       <p>{email}</p>
       <button onClick={() => setIsEditing(!isEditing)}>
        {isEditing ? 'Cancel' : 'Edit'}
       </button>
     </div>
   );
}

Functional components are more concise, easier to test, and better optimized for modern tooling.

Container and Presentational Components

This pattern separates data handling from UI rendering.

// Container component (handles data)
function UserProfileContainer() {
  const [userData, setUserData] = useState(null);
  const [loading, setLoading] = useState(true);

 useEffect(() => {
  fetchUserData().then(data => {
   setUserData(data);
   setLoading(false);
  });
 }, []);

 if (loading) return <LoadingSpinner />;
 return <UserProfileView user={userData} />;
}

// Presentational component (handles UI)
function UserProfileView({ user }) {
 return (
  <div className="user-profile">
    <h2>{user.name}</h2>
    <p>{user.email}</p>
  </div>
 );
}

Custom Hooks

Custom Hooks allow you to extract and reuse stateful logic between components.

function useMousePosition() {
 const [position, setPosition] = useState({ x: 0, y: 0 });

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

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

return position;
}

// Usage
function MouseDisplay() {
 const { x, y } = useMousePosition();
 return <p>Mouse position: {x}, {y}</p>;
}

Custom Hooks are more intuitive than older patterns like Higher-Order Components (HOCs) or render props, which often led to wrapper hell and prop collision issues.

2. State Management Patterns

Local Component State

For simpler components, local state using useState or useReducer is often sufficient:

function Counter() {
 const [count, setCount] = useState(0);

 return (
  <div>
   <h2>Count: {count}</h2>
   <button onClick={() => setCount(prev => prev - 1)}>-</button>
   <button onClick={() => setCount(prev => prev + 1)}>+</button>
  </div>
 );
}

For more complex state logic, useReducer provides a Redux-like approach within a component.

Context API

For a state that needs to be accessed by many components, the Context API provides a solution:

// Create and use a theme context
const ThemeContext = createContext();

function ThemeProvider({ children }) {
 const [theme, setTheme] = useState('light');

 return (
  <ThemeContext.Provider value={{ theme, setTheme }}>
   {children}
  </ThemeContext.Provider>
 );
}

// Custom hook for using the theme
function useTheme() {
 return useContext(ThemeContext);
}

// Usage in components
function ThemedButton() {
 const { theme, setTheme } = useTheme();

return (
 <button
  onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
  className={`btn-${theme}`}
 >
  Toggle Theme
 </button>
 );
}

External State Management

For complex applications, external libraries like Redux Toolkit, Zustand, or Jotai offer additional benefits:

// Zustand example (simpler than Redux)
import create from 'zustand';

  const useStore = create(set => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
  decrement: () => set(state => ({ count: state.count - 1 }))
}));

function Counter() {
 const { count, increment, decrement } = useStore();

 return (
  <div>
   <h2>Count: {count}</h2>
   <button onClick={decrement}>-</button>
   <button onClick={increment}>+</button>
  </div>
 );
}

Server State Management

Libraries like React Query or SWR simplify data fetching and caching:

// React Query example
function TodoList() {
 const { data: todos, isLoading } = useQuery('todos', fetchTodos);

 if (isLoading) return <div>Loading...</div>;

 return (
  <ul>
   {todos.map(todo => (
    <li key={todo.id}>{todo.text}</li>
   ))}
  </ul>
 );
}

These libraries handle caching, background refetching, and loading states automatically.

Related read: Boost Performance with React-Query: Managing Async State

3. Composition Patterns

Composition Over Inheritance

// Basic button component
function Button({ children, className, ...props }) {
 return (
  <button className={`button ${className || ''}`} {...props}>
   {children}
  </button>
 );
}

// Specialized buttons using composition
function PrimaryButton(props) {
 return <Button className="primary" {...props} />;
}

function IconButton({ icon, children, ...props }) {
 return (
  <Button {...props}>
   {icon} {children}
  </Button>
 );
}

Component: Composition with Children

The children prop allows for flexible composition:

function Card({ title, children }) {
 return (
  <div className="card">
   <div className="card-header">
    <h2>{title}</h2>
   </div>
   <div className="card-body">
    {children}
   </div>
  </div>
 );
}

// Usage
function App() {
 return (
  <Card title="User Profile">
   <p>Name: John Doe</p>
   <button>Edit Profile</button>
  </Card>
 );
}

Specialized Components

Create component families for better organization:

function Dialog({ children }) {
 return <div className="dialog">{children}</div>;
}

Dialog.Header = function({ children }) {
 return <div className="dialog-header">{children}</div>;
};

Dialog.Body = function({ children }) {
 return <div className="dialog-body">{children}</div>;
};

Dialog.Footer = function({ children }) {
 return <div className="dialog-footer">{children}</div>;
};

// Usage
function ConfirmDialog() {
 return (
  <Dialog>
   <Dialog.Header>Confirm Action</Dialog.Header>
   Dialog.Body>Are you sure?</Dialog.Body>
   <Dialog.Footer>
    <button>Cancel</button>
    <button>Confirm</button>
   </Dialog.Footer>
  </Dialog>
 );
}

4. Performance Optimization Patterns

Memoization

Use React’s memoization tools to prevent unnecessary re-renders:

// Component memoization
const MemoizedComponent = React.memo(function ExpensiveComponent({ data }) {
 // Expensive rendering logic
 return <div>{/* Rendered content */}</div>;
});

// Value memoization
function DataGrid({ items, filterText }) {
 // Only recalculates when dependencies change
 const filteredItems = useMemo(() =>
   items.filter(item => item.name.includes(filterText)),
   [items, filterText]
);

return (
  <div>
   {filteredItems.map(item => (
  <div key={item.id}>{item.name}</div>
   ))}
  </div>
 );
}

// Function memoization
function ParentComponent() {
  const [count, setCount] = useState(0);

// Function only recreated when dependencies change
const handleClick = useCallback(() => {
 console.log('Clicked!');
}, []);

return (
  <div>
   <p>Count: {count}</p>
   <button onClick={() => setCount(c => c + 1)}>Increment</button>
   <ChildComponent onButtonClick={handleClick} />
  </div>
 );
}

Code Splitting and Lazy Loading

React’s React.lazy and Suspense enable code splitting:

import React, { Suspense, lazy } from 'react';
// Lazy-loaded component
const Dashboard = lazy(() => import('./Dashboard'));

function App() {
 return (
  <div>
    <Suspense fallback={<div>Loading...</div>}>
     <Dashboard />
    </Suspense>
  </div>
 );
}

Virtualization for Large Lists

For long lists, virtualization renders only visible items:

import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
   <div style={style}>{items[index].name}</div>
  );

return (
 <FixedSizeList
   height={400}
   width={300}
   itemCount={items.length}
   itemSize={35}
 >
  {Row}
  </FixedSizeList>
 );
}

5. Architectural Patterns

Feature-based vs Type-based Folder Structures

Traditional type-based structure:

src/
 components/
 hooks/
 contexts/
 utils/
 pages/

Modern feature-based structure:
src/
 features/
  auth/
   components/
   hooks/
   context/
   utils/
  users/
   components/
   hooks/
   api.js
  shared/
   components/
   hooks/

A feature-based organization keeps related code together, improving maintainability as applications grow.

Atomic Design

Atomic Design breaks interfaces into five levels:

components/
 atoms/ # Basic building blocks (Button, Input)
 molecules/ # Simple combinations (SearchBar, FormField)
 organisms/ # Complex UI sections (Header, ProductCard)
 templates/ # Page layouts without specific content
 pages/ # Complete pages with real content

This methodology creates consistent interfaces and promotes reusability.

6. Common Anti-patterns and Pitfalls

Prop Drilling

Passing props through multiple components creates maintenance issues.

// Problematic
function App({ user }) {
 return <MainContent user={user} />;
}

function MainContent({ user }) {
 return <Sidebar user={user} />;
}

function Sidebar({ user }) {
 return <UserInfo user={user} />;
}

Solution: Use Context or state management for deeply shared data.

Huge Components

Components that do too much become difficult to maintain.

Solution: Break it down into smaller, focused components with single responsibilities.

Inline Function Definitions

Creating functions inside render can cause performance issues.

// Problematic
function SearchBar() {
 return (
 <input
  onChange={(e) => {
   // New function created on every render
   handleChange(e.target.value);
  }}
 />
 );
}

Solution: Use useCallback or define functions outside render.

Related read: Mastering React Native Apps: useMemo, useCallback, and FlashList

Direct DOM Manipulation

Bypassing React’s declarative approach can cause inconsistencies.

// Anti-pattern
useEffect(() => {
 document.getElementById('timer').innerText = `Count: ${count}`;
}, [count]);

Solution: Let React handle the DOM through state and props.

7. Decision Framework

When selecting from available React Design Patterns, ask:

1. Component Organization:

▪️Does this logic need to be shared across components?
▪️Is this component becoming too large?

2. State Management:

▪️Is this state only used by one component?
▪️How many components need to access this state?

3. Performance:

▪️Is this component rendering too often?
▪️Are there expensive calculations that could be memoized?

Pattern Tradeoffs

Pattern Tradeoffs

coma

Conclusion

React Design Patterns play a pivotal role in crafting scalable and maintainable applications. Modern React embraces functional components with Hooks over classes, composition over inheritance, custom Hooks for logic reuse, context for global state, and feature-based organization for large applications.

Remember that no pattern is perfect for all situations. The best approach often involves combining multiple patterns based on your specific needs. As React evolves, stay curious and adapt your approach to incorporate new best practices.

Keep Reading

Keep Reading

  • Service
  • Career
  • Let's create something together!

  • We’re looking for the best. Are you in?