21 Fantastic React Design Patterns and When to Use Them

Design patterns aren’t just for backend developers who can’t say a sentence without mentioning architecture or code quality. Frontend developers can benefit from them too, especially in React where component composition and hooks provide perfect opport…


This content originally appeared on DEV Community and was authored by Dennis Persson

Design patterns aren't just for backend developers who can't say a sentence without mentioning architecture or code quality. Frontend developers can benefit from them too, especially in React where component composition and hooks provide perfect opportunities to implement solid React design patterns and improve code architecture.

This article covers 21 design patterns and principles that will make your React code more maintainable, testable, and scalable. Each pattern includes a brief explanation and practical examples of when and how to use them.

In This Article

  • Core Design Patterns in React
    • Component Composition Pattern
    • Custom Hook Pattern
    • Control Props Pattern
    • Provider Pattern
  • Common React Design Patterns
    • Container Presentational Pattern
    • Compound Components Pattern
    • Headless Components
    • Atomic Design Pattern
    • Error Boundary Pattern
    • Portal Pattern
    • Render Props Pattern
    • Props Getters Pattern
  • Legacy Patterns
    • Higher Order Components Pattern (HOC)
  • General Patterns
    • DRY Principle
    • SOLID Principles
    • Dependency Injection
    • Separation of Concerns (SoC)
    • MVVM
    • Stable Dependency Principle (SDP)
    • KISS Principle
  • Design Patterns to Avoid
    • Clean Architecture
  • Note About Modern React Patterns and Frameworks

Core Design Patterns in React

These are the fundamental React design patterns that React is built upon. If you're writing React code, you're probably already using these patterns whether you know it or not. Sometimes we do good things even if we haven't read the book. These patterns form the bedrock of everything we build and are essential React best practices.

Code by the book
Lintlemore always codes by the book

Component Composition Pattern

Component composition is the very foundational React design pattern. It's the main way of thinking when writing React components, which is the very first design pattern you should read about when starting with React.

What the design pattern says is that an application is not a big monolith, it's a composition of tens or hundreds of components working together to form an application. Each component has its own reason to exist and can use, or be used by, one or several other components.

A component in this matter is essentially the same as a React component. Either a small leaf component in the component tree like a button, or a high-level component such as a whole page.

There are plenty of articles about component composition to find on Google and by GPTs, so I will not go into details here. But we will see in this article that a lot of the React design patterns serve their part to adhere to this pattern, where the Atomic Design Pattern might be the most obvious one. Every developer begins their journey with this fundamental pattern.

When to use: Always. This is the foundation of React development. I have sometimes seen React projects which barely use components and instead have a few large page-wide components. Doing that is to abuse React...

Custom Hook Pattern

If you have touched React, you probably have written some custom hooks. Doing that, means you are following the custom hook pattern, one of the most essential React best practices. If you aren't doing that, you are not writing proper React code.

Custom Hook Pattern means to lift out and encapsulate logic such as useEffects and useStates into custom hooks to abstract away the React logic in favor of readable hooks.

Too many React developers keep too much logic in their components, resulting in duplicated code, higher maintenance costs, and complex test cases. Following React best practices means separating concerns properly. The time it takes to break out logic from a component into a hook will almost certainly always save time.

Look at the component below, which fetches data from an API and renders it. It takes a brief moment to read it and understand it.

const PostsComponent = () => {
  const [posts, setPosts] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    const fetchPosts = async () => {
      try {
        const response = await fetch('/api/posts')
        const data = await response.json()
        setPosts(data)
      } catch (err) {
        setError(err)
      } finally {
        setLoading(false)
      }
    }

    fetchPosts()
  }, [])

  if (loading) return <div>Loading...</div>
  if (error) return <div>{error.message}</div>
  if (!posts) return null

  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  )
}

Now look at the same component below, but where the logic in the component has been lifted out to a hook.

// The hook's logic is easy to test
const usePosts = () => {
  const [posts, setPosts] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    const fetchPosts = async () => {
      try {
        const response = await fetch('/api/posts')
        const data = await response.json()
        setPosts(data)
      } catch (err) {
        setError(err)
      } finally {
        setLoading(false)
      }
    }

    fetchPosts()
  }, [])

  return { posts, loading, error }
}

// Main component is much easier to read
const PostsComponent = () => {
  const { posts, loading, error } = usePosts()

  if (loading) return <div>Loading...</div>
  if (error) return <div>{error.message}</div>
  if (!posts) return null

  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  )
}

Of course, it contains the same code. Except, this time, the usePosts hook name hints about what the useEffect and three useStates do, which made it a bit quicker to read.

And next time you re-use the usePosts hook for another component, you don't even have to read or write the code in that hook again.

At the same time, you now have a reusable hook which you only have to test once, and which is actually also very easy to test since it's a pure hook without any reason to check for what is being rendered.

If you test your UI components, the PostsComponent component might also need to be tested, and when doing that, you can simply mock the response of the usePosts hook instead of mocking the internals of the usePosts hook, such as the axios request, meaning that even the component test is now easier to write.

When to use: When you have reusable logic that multiple components need, or when your component is getting too complex. Potentially as a pattern to lift out logic from any component. Custom hooks is one of the most important concepts in React for maintainable code.

Hooktail spinoff
Some say the custom hook pattern originated from the Hooktail spinoff

Control Props Pattern

Props are passed to components all the time in React, that cannot be avoided. The Control Props Pattern is about whether your components are controlled or uncontrolled, which allows you to choose between passing more props or to have self-controlled components which work on their own using less props.

If a component has a state, it will need to be either controlled or uncontrolled, or both. It's more tricky to allow both - you will see it in UI component libraries and maybe write some of your own, but most of your own components will either be controlled or uncontrolled.

// Uncontrolled - manages its own state
const UncontrolledInput = () => {
  const [value, setValue] = useState('')

  return (
    <input 
      value={value} 
      onChange={(e) => setValue(e.target.value)} 
    />
  )
}

// Controlled - state managed by parent
const ControlledInput = ({ value, onChange }) => {
  return (
    <input 
      value={value} 
      onChange={onChange} 
    />
  )
}

Personally I prefer controlled components most often because uncontrolled ones are not as modular and reusable, since they are more difficult to override.

Remember the O in SOLID - components should not have to be modified, but be extendable. Uncontrolled components don't follow that pattern very well.

There are different arguments about what to use out there though. Uncontrolled components look nice and allow you to reuse the code within the component, but it might mean you need to write a new similar component instead of reusing the existing one, or modifying it for future needs.

When to use: When you need to control component state from a parent. For instance, if you have a form and need to read or modify fields in it, or if you have a context menu, dropdown menu or similar which you need to close programmatically from its outside.

Provider Pattern

The Provider Pattern is basically what React Contexts and useContext are. It's used in most major libraries to handle things like theming, routing, and global state management.

This is one of the most common React patterns for sharing state across components.

const ThemeContext = createContext()

// Provides the theme
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light')

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

// Hook to use the theme
const useTheme = () => {
  const context = useContext(ThemeContext)

  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider')
  }

  return context
}

// To provide the theme to the app
const App = () => {
  return <ThemeProvider>
    <AppContent />
  </ThemeProvider>
}

This pattern is everywhere in React development. React Router uses it for navigation context, styled-components uses it for themes, and Redux uses it for global state.

You will definitely use this pattern, mostly through libraries, but most apps also benefit from having a few custom-made ones.

Important caveat: Contexts are not for global state management, they'll create unnecessary re-renders. Use Zustand, Redux, or Jotai for proper state management instead. Neither should you overuse them as a standard dependency injection system.

When to use: When you need to share data for a component sub-tree without prop drilling, or when using third-party libraries that require context providers. Most common use case is to centralize app-wide configurations which rarely changes (themes, i18n, auth etc.).

Common React Design Patterns

These patterns are very often seen in React applications, but they're used as architectural choices rather than fundamental requirements. They are all widely used design patterns for building scalable React applications and improving code maintainability.

Container Presentational Pattern

Also called the Container Component Pattern, this separates your components into two categories: containers that handle logic and data, and presentational components that only take props and render them.

Container presentational pattern is probably the most common React design pattern for separating concerns.

// Presentational - only renders
const UserList = ({ users, onUserClick }) => (
  <div>
    {users.map(user => (
      <div 
        key={user.id} 
        onClick={() => onUserClick(user)}
      >
        {user.name}
      </div>
    ))}
  </div>
)

// Container - handles logic and data
const UserListContainer = () => {
  const { users } = useUsers()
  const navigate = useNavigate()

  const handleUserClick = (user) => {
    navigate(`/users/${user.id}`)
  }

  return <UserList 
    users={users} 
    onUserClick={handleUserClick} />
}

The presentational component is often stateless and reusable, while the container handles connections to other parts of the application, navigation, React contexts, and networking.

This pattern helps you control which components are responsible for fetching data and doing navigation, and which components you can freely reuse anywhere in your application.

Some might think that hooks like TanStack Query and SWR have made this pattern redundant, but it still solves important issues like the Single Responsibility Principle.

When to use: You would probably most often want to have this pattern in mind. You don't necessarily need to follow it strictly, but you want some pattern to separate your business logic from the UI and to build reusable components. This is the standard way to do that, along with the custom hook pattern.

Intelliton and Glimmabelle
Intelliton & Glimmabelle are for some difficult to tell apart, but one is all beauty and the other one all brain

Compound Components Pattern

If you are using a UI component library in React, chances are high that you will come across the Compound Components Pattern. This pattern can easily be confused with the Component Composition Pattern because of their similarities in name and purpose. They are not the same though.

While the Component Composition Pattern explains how to divide and conquer an application into smaller components, the Compound Components Pattern explains how to allow a set of components to work together as if they were a single component.

// Normal modal component
<Modal 
  isOpen={isOpen}
  title="Delete User"
  body="Are you sure you want to delete this user?"
  onConfirm={handleDelete}
  onCancel={handleCancel}
  confirmText="Delete"
  cancelText="Cancel"
/>

// Modal consisting of compound components
<Modal isOpen={isOpen}>
  <Modal.Header>Delete User</Modal.Header>
  <Modal.Body>
    Are you sure you want to delete this user?
  </Modal.Body>
  <Modal.Footer>
    <Button onClick={handleCancel}>Cancel</Button>
    <Button onClick={handleDelete}>Delete</Button>
  </Modal.Footer>
</Modal>

This pattern is mainly a way of solving the issues of prop drilling and the need for adding React contexts to share data between components that need to work together. As a bonus, it comes with great readability.

The main idea is that multiple components work together to achieve the functionality of a single entity.

When to use: It's great when building complex, flexible UI components that are highly customizable and where the components share a common state. Although it's a bit opinionated pattern, it's a design pattern that works well for component libraries. Can be a bit tricky to set up, but on the other hand, it's nice if some component lib owner has already implemented it for you.

Headless Components

Headless components pattern is used in some UI component libraries like Radix UI and Ark UI, they provide complex logic without any styling, leaving the visual presentation completely up to you.

This is one of the modern React design patterns that separates logic from presentation.

// Headless component of pure logic
const useDropdown = () => {
  const [isOpen, setIsOpen] = useState(false)
  const [selectedIndex, setSelectedIndex] = useState(-1)

  const toggleDropdown = () => setIsOpen(!isOpen)

  const selectItem = (index) => {
    setSelectedIndex(index)
    setIsOpen(false) 
  }

  return { 
    isOpen, 
    selectedIndex, 
    toggleDropdown, 
    selectItem 
  }
}

// Stylig is handled by the consumer
const Dropdown = () => {
  const { 
    isOpen, selectedIndex, 
    toggleDropdown, selectItem 
  } = useDropdown()

  return (
    <div className="dropdown">
      <button onClick={toggleDropdown}>
        {selectedIndex >= 0 
          ? `Item ${selectedIndex + 1}` 
          : 'Select an item'
        }
      </button>

      {isOpen && (
        <ul className="menu">
          <li 
            className="menu-item" 
            onClick={() => selectItem(0)}
          >
            Item 1
          </li>
          <li 
            className="menu-item" 
            onClick={() => selectItem(1)}
          >
            Item 2
          </li>
        </ul>
      )}
    </div>
  )
}

This pattern is perfect for those who are skeptical about choosing a UI library to lock into, because it will give you support for complex components which are cumbersome to write yourself without locking you into a specific style and look.

Headless components give you full control over styling and separate the concerns of UI logic and UI styling. It suits React very well, since hooks are mainly for logic and JSX for UI.

When to use: When you need complex component logic but want complete control over styling, or when building component libraries that need to be framework agnostic. If you like to follow best practices and build a React architecture that scales, this design pattern is for you.

When not to use: Don't over-engineer simple components with headless patterns. This pattern is for more logically complex components. It's completely unnecessary for a simple button or switch toggle.

Bareboneux
Bareboneux lives its life inside its shell, it doesn't care if you would paint it pink or poke one of its spikes

Atomic Design Pattern

The Atomic Design Pattern is one of the newer kids on the block, inspired by chemistry, probably because web development wasn't complicated enough already.

It's a pattern for creating design systems that organizes components into a clear hierarchy: atoms, molecules, organisms, templates, and pages. This design pattern is both easy to understand and also very scalable.

You start with the smallest pieces (atoms), combine them into slightly larger pieces (molecules), then build complex structures (organisms), and finally create complete layouts (templates and pages).

// Atoms - basic building blocks
const Button = ({ children, onClick }) => (
  <button className="btn" onClick={onClick}>{children}</button>
)

const Input = ({ value = '' }) => (
  <input className="input" value={value} />
)

// Molecules - combinations of atoms
const SearchBox = ({ searchText, onSearch }) => (
  <div className="search-box">
    <Input value={searchText} />
    <Button onClick={onSearch}>Search</Button>
  </div>
)

// Organisms - complex UI components
const Header = () => (
  <header className="header">
    <Logo />
    <SearchBox onSearch={handleSearch} />
    <Navigation />
  </header>
)

// Templates & Pages - full layouts
const HomePage = () => (
  <div>
    <Header />
    <main>
      <HeroSection />
      <SomeContent />
    </main>
    <Footer />
  </div>
)

This pattern helps you think systematically about your UI components and creates a clear hierarchy that's easy to understand and maintain. It's particularly useful for larger applications where you need consistency across different parts of the UI.

The key insight is that by organizing components this way, you can ensure that your design system is both consistent and scalable. Atoms are reused everywhere, molecules combine atoms in meaningful ways, and organisms create complex but reusable UI sections.

Just don't get too carried away with the chemistry metaphor. Your components don't need to have atomic numbers, and you definitely don't need to create a periodic table of UI elements (though I've seen it attempted).

When to use: When building large applications that need a consistent design system, or when working with teams that need clear component organization guidelines. I would recommend having this mindset even if you don't officially follow it. It's one of the React design patterns that works well for React applications which need a scalable design system.

Atomunculus
Don't be fooled by Atomunculus's size; even a black hole looks insignificant from the outside

Error Boundary Pattern

Ever had that moment where one tiny bug in your React app brings down the entire application? One component throws an error, and suddenly your users are staring at a blank screen wondering if the internet broke.

This is exactly the problem error boundaries solve. They're React's way of catching JavaScript errors anywhere in the component tree and preventing the entire app from crashing.

An error boundary would look something like this, or if you use any of the hook libraries for it.

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false }
  }

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

  componentDidCatch(error, errorInfo) {
    console.error('Error caught by boundary:', error, errorInfo)
  }

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

    return this.props.children
  }
}

const App = () => (
  <ErrorBoundary>
    <UserProfile />
    <ProductList />
  </ErrorBoundary>
)

Error boundaries only catch errors in the components below them in the tree. They don't catch errors in:

  • Event handlers (use try-catch instead)
  • Async code like setTimeout and promises
  • Server-side rendering
  • Code that runs outside the component tree

For these cases, you'll need try-catch blocks or other error handling strategies.

The key insight is to place error boundaries strategically around parts of your app that could fail independently. You might have one around your main app, another around the user dashboard, and another around some interactive AI chat.

When to use: A pattern which should be used more than it is. Most applications I have seen don't really use error boundaries, but they do serve their purpose to build stable applications. They are perfect safe-guards for applications which has several independent features to ensure that the whole app doesn't crash just because one of the features fails.

Portal Pattern

You aren't a real frontend developer if you haven't built a context menu or dropdown that got clipped by its parent container, or struggled with z-index issues where a modal appears behind other elements.

This is the problem portals solve. They provide a way to render children into a DOM node that exists outside the parent component's DOM hierarchy. It's a magic door that lets your UI elements escape their container and appear exactly where you want them.

import { createPortal } from 'react-dom'

const Modal = ({ isOpen, onClose, children }) => {
  if (!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}
        >×</button>
        {children}
      </div>
    </div>,
    document.body
  )
}

const App = () => {
  const [isModalOpen, setIsModalOpen] = useState(false)

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

      <Modal 
        isOpen={isModalOpen} 
        onClose={() => setIsModalOpen(false)}
      >
        <h2>Modal Content</h2>
        <p>Rendered in body, not in parent</p>
      </Modal>
    </div>
  )
}

Portals are particularly useful when you need to render content that should appear above everything else, like modals or notifications. They solve CSS z-index issues and ensure your modal doesn't get clipped by parent containers with overflow: hidden.

The pattern is also useful for tooltips and dropdowns that need to break out of their parent's overflow constraints. Just remember that even though the DOM node is different, the React event bubbling still works as expected.

The alternative to using a portal would be to fiddle with z-indexes and their stacking contexts. And honestly, most web developers don't really know how stacking contexts work for z-indexes, which is why these kinds of issues are severely common.

When to use: When building modals, tooltips, context menus, dropdowns, etc. which need to escape their parent container's CSS constraints. It's one of the common React patterns for building overlays and popups. If you see your component being cut-off with a z-index issue the portal might the solution.

Voidnibbler
If you use portals frequently, you might one day spot the Voidnibbler, but no one would believe you

Render Props Pattern

The Render Props Pattern is where you pass a function as a prop that returns JSX. You will use it, otherwise you are probably overusing other design patterns like the Compound Components Pattern.

It feels kinda ugly at first, but it's completely normal and quite powerful.

const TodosFetcher = ({ url, render }) => {
  const [todos, setTodos] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch(url)
      .then(response => response.json())
      .then(data => {
        setTodos(data)
        setLoading(false)
      })
  }, [url])

  return render({ todos, loading })
}

const App = () => (
  <TodosFetcher 
    url="/api/todos" 
    render={({ todos, loading }) => (
      loading 
        ? <div>Loading...</div> 
        : (
          <div>
            {todos.map(todo => (
              <div key={todo.id}>{todo.name}</div>
            ))}
          </div>
        )
    )}
  />
)

The pattern allows you to share code between components while giving consumers control over what to render. It's particularly useful when you have complex logic that needs to be shared but the UI might vary.

In reality, the App component code would be created with its own components utilizing proper component composition:

const LoadingSpinner = () => <div>Loading...</div>

const TodoList = ({ todos }) => (
  <div>
    {todos.map(todo => (
      <div key={todo.id}>{todo.name}</div>
    ))}
  </div>
)

const App = () => (
  <TodosFetcher
    url="/api/todos" 
    render={({ todos, loading }) => (
      loading 
        ? <LoadingSpinner /> 
        : <TodoList todos={todos} />
    )}
  />
)

This example shows how the render props pattern works beautifully with component composition. Each UI concern (loading, data display) is its own focused component, making the code much more readable and maintainable.

While you could put all these components in the same file as your App component, it's better practice to place them in their own files for better organization and reusability.

When to use: If you prefer to use it. Custom hooks and compound components and props getter pattern have many times replaced this pattern, but it still has its use cases.

Props Getters Pattern

The Props getters pattern is similar to the render props pattern we just read about. Instead of passing a function which renders UI components you use a function to provide the props a component needs.

You'll see this pattern in libraries like React Hook Form, where instead of manually setting up all the form props, you get functions that return perfectly configured prop objects.

const useToggle = (initialValue = false) => {
  const [on, setOn] = useState(initialValue)

  const toggle = () => setOn(!on)

  const getTogglerProps = (props = {}) => ({
    onClick: toggle,
    'aria-pressed': on,
    ...props
  })

  const getIndicatorProps = (props = {}) => ({
    children: on ? 'On' : 'Off',
    ...props
  })

  return { on, toggle, getTogglerProps, getIndicatorProps }
}

const Toggle = () => {
  const { on, getTogglerProps, getIndicatorProps } = useToggle()

  return (
    <div>
      <button {...getTogglerProps()}>
        Toggle
      </button>
      <div {...getIndicatorProps()} />
    </div>
  )
}

As you can see, we have use a hook which provides us with the data we need instead of a component as we had with the TodosFetcher. We have also baked in some props getters in that hook which return the props we commonly would use together with our data.

If we would want to, we can easily override the properties, but most often we can trivially use the prop getter function as is to get the props we need for our UI elements.

This is particularly useful when you have complex components that need to manage multiple related props. Instead of the consumer having to know about all the internal state and configuration, they just call the appropriate getter function and get exactly what they need.

When to use: When building reusable components that need to manage complex prop configurations and accessibility attributes. Not seen too often, but kinda useful.

Legacy Patterns

Some patterns were used extensively in React in the past, but have been phased out by time and passed on the torch to other patterns.

It doesn't mean they are anti-patterns, but their best before dates have been reached already.

Higher Order Components Pattern (HOC)

The HOC pattern is the old way of lifting out reusable code in React, it was one of the most fundamental design pattern for React for quite a while.

It was mainly used with the old class components before the functional components came with hooks. It did live on for a while after the hooks came, but is more rarely seen today.

A typical example of a HOC would be a withAuth hook, which injects a user into a component.

const withAuth = (Component) => {
  return (props) => {
    const { user } = useContext(AuthContext)

    if (!user) {
      return <LoginComponent />
    }

    return <Component {...props} user={user} />
  }
}

const Profile = ({ user }) => {
  return <div>Welcome, {user.name}!</div>
}

// withAuth provides the user to Profile component
const ProfileWithAuth = withAuth(Profile)

Today, this hook would most often instead be a custom hook, looking like this. It's a bit more readable and also easier to test.

const useAuth = () => {
  const { user } = useContext(AuthContext)

  if (!user) {
    // Redirect to login
  }

  return { user }
}

const Profile = () => { 
  const { user } = useAuth()
  return <div>Welcome, {user.name}!</div>
}

When to use: Legacy pattern - prefer custom hooks instead. Only use when working with older codebases or class components that can't be easily converted.

Overcloak
Overcloak is from an old time, from an era all apprentices obeyed him

General Patterns

These patterns are less commonly discussed together with React, but very often discussed among backend developers, who we know usually cannot say a sentence without mentioning design patterns, architecture, or code quality.

However, it definitely doesn't mean that you shouldn't care about these. If you already know React well, these are probably the most important patterns to read about to become a better React developer.

Perfect architecture meme
Would it be a good time to tell him the project was cancelled two months ago?

DRY Principle

This principle is more nuanced than most developers realize - it's not a black or white pattern you can generally deem to be good or bad. While DRY is many times good, over-applying it can create more problems than it solves.

Some clear candidates for DRY implementation include atomic components like buttons, inputs, and icons that get reused everywhere. Utility functions such as formatters, validators, and helpers are also perfect since they're pure functions that solve common problems. API integration logic when you're making the same requests in multiple places, and complex state management when multiple components need identical state logic.

But be cautious using this pattern with code which might diverge. I've seen codebases that became impossible to navigate because developers created layers upon layers of abstraction for code that was only used once. Simple solutions became incomprehensible mazes of interfaces, factories, and abstract base classes for the only reason to potentially be able to save a few duplicated lines in the future.

The key insight is that DRY should be applied thoughtfully, not religiously. Sometimes duplication is actually cheaper than the wrong abstraction. It's often easier to extract common patterns later when you have 2-4 similar implementations than to guess what the abstraction should look like upfront.

Modern development tools also change the equation. With AI-powered code completion and refactoring tools, eliminating duplication might literally be one prompt away. This means the cost of both creating and maintaining DRY code has decreased significantly.

When to use: When you have truly identical logic that is used in multiple places and unlikely to diverge, or when building reusable utility functions and components. The core of the app should be kept as DRY as possible, while the outer top layer of the application is perfectly fine if it's WET. You are very likely to change that code any day.

Deduplica
Deduplica would probably benefit from a slightly more WET environment

SOLID Principles

The SOLID principles are fundamental design patterns that apply beautifully to React development. While they're often discussed in backend contexts, they're incredibly valuable for building maintainable React applications.

Single Responsibility Principle (SRP)

Each component and hook should have one clear responsibility. Developers violate this principle constantly when they fetch data, handle validations, manage state, perform calculations, and render complex UI all in a single massive component.

Components like that are impossible to test, difficult to reuse, and a nightmare to debug. The Container Component Pattern and custom hooks are your best friends for solving SRP violations.

When to use: Always. This is the foundation of clean, maintainable React code. Break down complex components into smaller, focused pieces.

Open/Closed Principle (OCP)

Components should be open for extension but closed for modification. This sounds fancy, but it's actually quite simple - you want to be able to add new features without breaking existing code.

React's component composition model can help with this if we design our components appropriately. The key is to follow two simple rules:

  • Prefer to compose multiple small components over building large monolith-components
  • Make commonly used components configurable through props

An example of the second rule could be a component for a delete button. That's a normal button which might be red and have a "Delete" text on it. You wouldn't hardcode its text or color in the button, rather you would pass a text and type prop to it so you can reuse the same button as a cancel or OK button without having to update the button component.

By following these rules you won't have a need to modify old components very often and can normally reuse old components to compose new and more advanced components.

In codebases where these rules aren't followed, developers consistently need to modify already existing components and rewrite their tests, which is both time consuming and prone to cause bugs in old features which previously have been proven to work.

When to use: Make sure to follow React's component composition model and you will naturally build reusable components which are open for extension but closed for modifications.

Liskov Substitution Principle (LSP)

Liskov mentions that components that extend or inherit from other components should be completely interchangeable with their base components. For instance, that principle applies if you would have a Button component and then also sub-components as PrimaryButton and SecondaryButton. Liskov then says that the sub-component should take at least the same props as the Button component.

The practical benefit of this is that you can refactor and exchange components without worrying about breaking consumers. It might not be super common to use inheritance like this in React, since React by nature uses composition instead of inheritance, but the principle tells something important about interfaces.

Even if you don't have a hierarchy of buttons you might have multiple clickable components, such as buttons, chips, toggles and so on. Or a set of different dropdown components like a list one, a searchable one, one with check boxes etc.

If you have similar components like that, keep their interfaces similar so it's easy to exchange them if needed. You never know when that UX designer changes its mind.

When to use: I wouldn't recommend to use actual inheritance as the LSP is about. However, when creating a set of components which all has similarities, then ensure to maintain the same or a similar interface and behavior for all of them.

Interface Segregation Principle (ISP)

Many projects have some of those super components/hooks which are used for nearly every component in the app. It has 15+ props and your only mission is to figure out which of those to use in your case, and most of your day goes to figuring out how to retrieve all data you need to even get it to compile without type errors.

That's a typical violation of the ISP which says that components shouldn't be forced to depend on props they don't use. It's better to use multiple hooks with fewer props and combine them for your use case. Or, to look over all the solution in general and see if it can be simplified. Chances are that someone has chosen a poor architectural solution there and that half of the props naturally could be refactored away.

When to use: When designing component interfaces, keep them focused and specific to what each component actually needs, avoiding bloated prop interfaces.

Dependency Inversion Principle (DIP)

Dependency inversion principle tells that high-level components shouldn't depend on low-level implementation details. In short, this means that you should layer your application with abstractions in a way that high-level components don't depend on low-level functionality.

For instance, if you have a component of a user list, then that component shouldn't contain the logic to fetch the users. Instead it should depend on some hook which fetches the users for it, or get the users injected to it via a state manager or similar. Or more precisely, depend on an interface of how to get retrieve the data with clearly defined TypeScript attributes.

In that way, you can replace a low-level axios request in favor of fetch or add a fancy hook like SWR without out having to refactor your user list component, since the user list component only depends on an abstract hook like useUsers rather than having axios-specific logic within it.

When to use: When building components that need to work with different data sources or implementations, or when you want to make your code more testable and flexible. Chances are that you might replace low-level functionality in your code base some day.

For detailed examples and implementation strategies, see my comprehensive article on writing SOLID React hooks.

Chubby
Chubby has a lot to say, for a good reason

For detailed examples and implementation strategies, see my article on writing SOLID React hooks.

Dependency Injection

While not as common in React as in backend development, dependency injection can be achieved through Context API and custom hooks. Context API is the obvious choice, but hooks can also solve parts of the same issues.

Dependency injection is also covered in my article about SOLID React hooks.

When to use: When you need to swap implementations for testing or when building applications that need to work with different services or APIs.

Separation of Concerns (SoC)

Separation of Concerns is about organizing code so that different types of logic are kept separate and focused. While it's related to the Single Responsibility Principle (SRP), SoC operates at a higher architectural level.

SRP says each component should have one responsibility, while SoC says different types of concerns (data, business logic, presentation) should be in separate layers with clear boundaries.

Below is a typical violation of SoC. You don't really have to read it, I will spare you from that headache. The important thing to realize is that you hesitate from even reading it.

// Bad - all concerns mixed together
const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(false)
  const [editing, setEditing] = useState(false)
  const [formData, setFormData] = useState({})

  useEffect(() => {
    setLoading(true)
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(user => {
        setUser(user)
        setLoading(false)
      })
  }, [userId])

  const handleSave = async () => {
    const updatedUser = {
      ...user,
      ...formData,
      displayName: `${formData.firstName} ${formData.lastName}`,
      isActive: formData.status === 'active'
    }

    const response = await fetch(`/api/users/${userId}`, {
      method: 'PUT',
      body: JSON.stringify(updatedUser)
    })

    if (response.ok) {
      setUser(updatedUser)
      setEditing(false)
      toast.success('User updated successfully')
    }
  }

  if (loading) return <div>Loading...</div>
  if (!user) return <div>User not found</div>

  return (
    <div>
      {editing ? (
        <div>
          <input 
            value={formData.firstName || user.firstName}
            onChange={(e) => setFormData({...formData, firstName: e.target.value})}
          />
          <select 
            value={formData.status || user.status}
            onChange={(e) => setFormData({...formData, status: e.target.value})}
          >
            <option value="active">Active</option>
            <option value="inactive">Inactive</option>
          </select>
          <button onClick={handleSave}>Save</button>
        </div>
      ) : (
        <div>
          <h2>{user.displayName}</h2>
          <p>Status: {user.isActive ? 'Active' : 'Inactive'}</p>
          <button onClick={() => setEditing(true)}>Edit</button>
        </div>
      )}
    </div>
  )
}

This component is doing everything: fetching data, transforming it, managing UI state, handling form logic, and rendering. If any of these concerns change, you have to modify this component.

The SoC design principle can be applied by separating each concern into its own layer:

// Presentation layer - only rendering
const UserProfile = ({ userId }) => {
  const { user, loading } = useUserState(userId)

  if (loading) {
    return <LoadingSpinner />
  }

  if (!user) { 
    return <NotFound message="User not found" />
  }

  return <UserDisplayCard user={user} />
}
// State management layer
const useUserState = (userId) => {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(false)

  const { getUser } = useUserApi()
  const { transformUserForDisplay } = useUserBusinessLogic()

  const loadUser = async () => {
    setLoading(true)

    try {
      const userData = await getUser(userId)
      setUser(transformUserForDisplay(userData))
    } finally {
      setLoading(false)
    }
  }

  useEffect(() => {
    loadUser()
  }, [userId])

  return { user, loading }
}
// Business logic layer - data transformations
const useUserBusinessLogic = () => {
  const transformUserForDisplay = (user) => ({
    ...user,
    displayName: `${user.firstName} ${user.lastName}`,
    isActive: user.status === 'active'
  })

  return { transformUserForDisplay }
}
// Data layer - API communication
const useUserApi = () => {
  const getUser = async (userId) => {
    const response = await fetch(`/api/users/${userId}`)
    return response.json()
  }

  return { getUser }
}

Now each layer has a clear, focused responsibility:

  • Data layer: Knows how to communicate with APIs
  • Business logic layer: Handles data transformations and business rules
  • State management layer: Orchestrates data and state changes
  • Presentation layer: Only concerned with rendering UI

This separation makes the code much easier to understand, test, and maintain. You can test business logic without worrying about API calls or rendering. You can change the API structure without touching business logic. And you can redesign the UI without affecting data handling.

The key insight is that different types of logic change for different reasons and at different rates. By keeping them separate, you minimize the ripple effects when changes are needed.

When to use: Any non-trivial application should separate types of logic like this. It's necessary for the project to scale and to work effectively in a team.

MVVM

MVVM (Model-View-ViewModel) is very popular in many frameworks but rarely spoken about in React. Yet it can be very useful, either as a whole or taking inspiration from it.

You can read my detailed article about implementing MVVM in React for a complete guide on how to structure larger React applications using this pattern.

When to use: Wouldn't say it's common to follow this pattern in React and neither would I recommend to follow it strictly. But many apps take inspiration from it and use similar layerings. The goal is to separate the concerns in some scalable way.

Mooview
Who doesn't like Mooview? Look at those puppy eyes!

Stable Dependency Principle (SDP)

There's always components and hooks in a React app which are stable and well tested - SDP says that those should form the foundation of our app.

Volatile components will of course exist, which are often updated or replaced. The point with SDP is to avoid having other components being dependent on these risky and unstable components to avoid having the whole app to crash for a small change in a seemingly unimportant component.

For instance, browser's built-in functionality rarely changes - that is safe to depend on. A useLocalStorage hook in React which uses local storage should rarely change and can also be considered stable.

On the other hand, the useStoreAnythingYouWant hook my colleague Brad implemented might be more likely to change - consider it unstable (but don't let Brad know I said that).

The point is, avoid using stuff that you know might change. For example, private variables in libraries often start with an underscore. They are marked that way because they might change without any notice. If you see it, don't use it!

In e2e tests with Cypress or similar, use data-test-id attributes, don't use CSS selectors to grab HTML elements or class names. CSS classes disappears just as quickly as Brad when he sees people walking towards the table tennis room.

When to use: Always. Choose stable, well-established dependencies over experimental or frequently changing ones to avoid breaking changes in your application.

KISS Principle

The last principle in this list is very simple, but it would be stupid not to follow. The KISS principle stands for "Keep It Simple, Stupid".

When designing any kind of system, it should always be simple. Don't overcomplicate things! Keep the user interface simple, and the code as well. Think one extra time if you really need to do the things you are trying to do.

The things you might not need to add can be features which might give little value but introduce more complexity to the application. Or it might be a custom-made user authentication mechanism, instead of using a third-party provider for authentication.

Or it might be a library which comes with extra configuration files and a bundle size of 100 kB extra, when you really only use one or two functions from that library.

This complexity can also be in the code itself. A perfect example of how to overcomplicate code in React is to use useEffect when it isn't needed.

const SomeComponent = () => {
  const [fetchData, setFetchData] = useState(false)
  const [data, setData] = useState()

  useEffect(() => {
    if (fetchData) {
      const dataFromSomewhere = getData()
      setData(dataFromSomewhere)
    }
  }, [fetchData])

  return (
    <>
      <button 
        onClick={() => setFetchData(true)}
      >Fetch data</button>
      <div>{data}</div>
    </>
  )
}

It's very common that developers use useEffect in scenarios like the one above. In reality, the code would work much better with the simple solution below.

const SomeComponent = () => {
  const [data, setData] = useState()

  const fetchData = () => {
    const dataFromSomewhere = getData()
    setData(dataFromSomewhere)
  }

  return (
    <>
      <button 
        onClick={() => fetchData()}
      >Fetch data</button>
      <div>{data}</div>
    </>
  )
}

In my article about reactive developers, you can see more complicated code React developers tend to write when it really isn't necessary, and what the consequences of that are.

When to use: Always. Prefer simple, straightforward solutions over complex ones unless complexity provides clear, significant benefits. This might not sound like a design pattern for many, but it's one of the most trivial and effective design pattern which prevents over-engineering.

Design Patterns to Avoid

This is of course opinionated, but be aware of my opinion, because I'm definitely not alone with this opinion. This is important to know when choosing React design patterns for your project. Some forces of nature should remain untamed and preserved in their natural domain.

Clean Architecture

I'm listing this here because I have seen backend developers using it in React. MVVM I can feel is alright, but Clean Architecture is a step too much for React.

The problem is kind of too many layers and you will annoy the hell out of your normal frontend colleagues. Plus, it's often a pure rip-off of Clean Architecture that breaks quite some other core design patterns of React.

React already has its own architectural patterns that work well. Don't take architectural patterns from your previous work experience and force them into new code bases where they don't belong. Stick to React best practices and common React patterns instead. Each ecosystem has its own natural laws.

Modulynx
Modulynx is quite tiny, which allows her to clean the tiniest interfaces

Note About Modern React Patterns and Frameworks

If you don't think 21 patterns was enough, you might want to stay tuned, there are actually many new patterns which came with React 18 and 19.

You might have familiarized yourself with them already, especially if you are using frameworks like Next.js or Remix.

I haven't included any of those in this article though, since it's already long enough. But for the impatient readers I can give some spoilers, it's about functionalities like:

  • Server Components
  • Suspense data fetching
  • Concurrent features

These patterns are still evolving, but they represent the future direction of React development. Regardless whether you are using one of the more prominent React
frameworks or if you use pure React Vite.

React Vite
Because I love React, I am particularly fond of Vite

 


This content originally appeared on DEV Community and was authored by Dennis Persson


Print Share Comment Cite Upload Translate Updates
APA

Dennis Persson | Sciencx (2025-08-24T14:41:00+00:00) 21 Fantastic React Design Patterns and When to Use Them. Retrieved from https://www.scien.cx/2025/08/24/21-fantastic-react-design-patterns-and-when-to-use-them/

MLA
" » 21 Fantastic React Design Patterns and When to Use Them." Dennis Persson | Sciencx - Sunday August 24, 2025, https://www.scien.cx/2025/08/24/21-fantastic-react-design-patterns-and-when-to-use-them/
HARVARD
Dennis Persson | Sciencx Sunday August 24, 2025 » 21 Fantastic React Design Patterns and When to Use Them., viewed ,<https://www.scien.cx/2025/08/24/21-fantastic-react-design-patterns-and-when-to-use-them/>
VANCOUVER
Dennis Persson | Sciencx - » 21 Fantastic React Design Patterns and When to Use Them. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/08/24/21-fantastic-react-design-patterns-and-when-to-use-them/
CHICAGO
" » 21 Fantastic React Design Patterns and When to Use Them." Dennis Persson | Sciencx - Accessed . https://www.scien.cx/2025/08/24/21-fantastic-react-design-patterns-and-when-to-use-them/
IEEE
" » 21 Fantastic React Design Patterns and When to Use Them." Dennis Persson | Sciencx [Online]. Available: https://www.scien.cx/2025/08/24/21-fantastic-react-design-patterns-and-when-to-use-them/. [Accessed: ]
rf:citation
» 21 Fantastic React Design Patterns and When to Use Them | Dennis Persson | Sciencx | https://www.scien.cx/2025/08/24/21-fantastic-react-design-patterns-and-when-to-use-them/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.