6 React Design Patterns You Must Know!

6 React Design Patterns You Must Know!

Keep yourself informed about the most current React Design Patterns.

Introduction

Imagine, You're working on a complex React application, juggling multiple components, handling state management, and trying to ensure optimal performance. As your codebase grows, you start encountering common challenges such as code repetition, difficulty in managing state across components, and the constant struggle to keep your application maintainable and scalable. The once-exciting development process now feels overwhelming, and you find yourself spending more time debugging and refactoring than actually building new features. Sounds familiar?

Enter React Design Patterns. These ingenious solutions offer a lifeline to developers grappling with the complexities of modern web development. By implementing these patterns, you gain a set of proven techniques to overcome common hurdles and streamline your development workflow. React Design Patterns not only make your code more organized and efficient but also save you precious time and effort, allowing you to focus on crafting exceptional user experiences.

The Power of React Design Patterns:

React Design Patterns are like a secret arsenal, empowering developers to conquer coding challenges with ease. They provide structured approaches to common problems, enabling you to write clean, reusable, and maintainable code. Let's take a closer look at how React Design Patterns can transform your development experience:

  1. Eliminating Code Repetition: One of the most significant pain points in software development is code duplication. React Design Patterns, such as the Container/Presentational Pattern, help you separate concerns and modularize your code. By abstracting complex logic into reusable containers and keeping presentation components focused solely on rendering, you can eliminate code redundancy and improve code readability.

  2. Simplifying State Management: As applications grow in complexity, managing state across components becomes increasingly challenging. React Design Patterns like the Higher Order Component (HOC) Pattern and Render Props Pattern offer elegant solutions to share state and behavior between components. These patterns facilitate component composition and enable better control over state management, making your application more predictable and maintainable.

  3. Unlocking the Power of Hooks: React Hooks, introduced in React 16.8, have revolutionized the way we write React components. The Hooks Pattern allows you to leverage the full potential of Hooks, such as useState, useEffect, and useContext, to streamline your code and eliminate class-based components. With Hooks, you can achieve cleaner, more concise code that is easier to understand and maintain.

  4. Providing Contextual Data: The Provider Pattern, often used in combination with useContext Hook, enables you to share data and functionality across the component tree without the need for prop drilling. By establishing a global context, you can access and modify data from any component within the hierarchy, simplifying communication and reducing unnecessary prop passing.

  5. Creating Compound Components: Some user interfaces consist of multiple components that work together as a cohesive unit. The Compound Pattern allows you to create compound components that encapsulate related functionality and provide a unified interface to the user. This pattern enhances reusability and makes it easier to manage complex components as a single entity.

By embracing these React Design Patterns, you'll not only overcome common development challenges but also enhance code quality, improve productivity, and foster collaboration within your development team. So, let's dive deep into each design pattern and discover how they can transform your React development journey.

In the following sections, we will explore the six indispensable React Design Patterns: Container/Presentational Pattern, HOC Pattern, Render Props Pattern, Hooks Pattern, Provider Pattern, and Compound Pattern. Through practical examples and real-world use cases, we will unravel the power of each pattern and equip you with the knowledge to implement them effectively in your own projects.

Get ready to revolutionize your React development experience as we embark on this enlightening journey through the world of React Design Patterns.

Container/Presentational Pattern

The Container/Presentational Pattern, also referred to as Smart/Dumb Components or Stateful/Stateless Components, is a powerful design pattern that promotes a clear separation of concerns within your React application. This pattern enables you to modularize your code, enhance code reusability, and improve overall maintainability.

Understanding the Container/Presentational Pattern:

In the Container/Presentational Pattern, components are categorized into two distinct roles: containers and presentational components. Let's explore each role in detail:

  1. Containers (Smart Components): Containers, also known as Smart Components, are responsible for handling the logic and data-related operations within your application. They encapsulate complex business logic, data fetching, and state management, making them the brain of your application. Containers interact with APIs, perform calculations, and manage the application's state.

    Here's an example of a container component that manages the state and data fetching:

     import React, { useState, useEffect } from 'react';
     import API from './lib/API';
     import UserDetails from './UserDetails';
    
     const UserContainer = () => {
       const [user, setUser] = useState(null);
    
       useEffect(() => {
         // Fetch user data from an API
         const getUSerData = async() => {
             const data = await API.getUSerData();
             setUser(data);
         }
         getUSerData();
    
       }, []);
    
       if (!user) {
         return <div>Loading...</div>;
       }
    
       return <UserDetails user={user} />;
     };
    
     export default UserContainer;
    

    Here, UserContainer functional component has a state user whose value is set by setUser inside useEffect hook after settling the response of API.getUser() method. This component is returning another component UserDetails on which user state data is passed as a prop representing the presentational component and it will deal with the UI logic.

  2. Presentational Components (Dumb Components): Presentational Components, also known as Dumb Components, focus solely on rendering the UI elements. They receive props from containers or parent components and display the data or user interface accordingly. Presentational Components are concerned with how things look and provide a visual representation of the application's state.

    Here's an example of a presentational component that receives props and renders the user details:

     import React from 'react';
    
     const UserDetails = ({ user }) => {
       return (
         <div>
           <h2>User Details</h2>
           {user ? (
             <div>
               <p>Name: {user.name}</p>
               <p>Email: {user.email}</p>
               {/* Display additional user details */}
             </div>
           ) : (
             <div>No user found.</div>
           )}
         </div>
       );
     };
    
     export default UserDetails;
    

    Here, UserDetails component is dealing with the UI logic. It receives the data as user prop and combine its values with UI logic.

Implementing the Container/Presentational Pattern:

To implement the Container/Presentational Pattern, follow these guidelines:

  1. Identify Components: Identify which components will handle data operations and state management (containers) and which components will focus solely on rendering the UI (presentational components).

  2. Create Containers: Create containers by encapsulating the data-related logic, including state management, API calls, and data transformations. Containers pass down the required data and functions as props to the presentational components.

  3. Develop Presentational Components: Develop presentational components that receive props from containers and use them to render the UI elements. Presentational components don't manage state or perform complex data operations but rather focus on displaying the data provided by the containers.

  4. Establish Communication: Establish communication between containers and presentational components using props. Containers pass data, callbacks, and other required information to the presentational components, enabling them to render the UI correctly.

Benefits of the Container/Presentational Pattern:

  1. Separation of Concerns: By separating the logic and presentation layers, this pattern promotes a clean and organized codebase. Containers handle the data-related operations, while presentational components focus on rendering the UI. This separation enhances code maintainability and makes it easier to understand and modify each component's functionality.

  2. Code Reusability: With the Container/Presentational Pattern, presentational components become highly reusable. They can be easily plugged into different containers or used across various parts of the application. This reusability reduces code duplication and improves development efficiency.

  3. Scalability: As your application grows, the Container/Presentational Pattern helps maintain a scalable codebase. By extracting complex logic into containers, you can easily manage the application's state and handle data operations efficiently. Presentational components remain lightweight and focused solely on rendering, ensuring your UI remains responsive and performant.

By adhering to the Container/Presentational Pattern, you can significantly enhance the organization, reusability, and maintainability of your React application. This pattern allows you to build robust and scalable applications by separating concerns and leveraging the strengths of both container and presentational components.

Higher Order Components (HOCs)

The Higher Order Component (HOC) Pattern is a versatile and widely used design pattern in React that allows you to reuse component logic and enhance code modularity. By using HOCs, you can extract common functionality from components and apply it to multiple components without repeating code. Let's dive into the details of the HOC Pattern and explore how it can be implemented.

Understanding the Higher Order Component (HOC) Pattern:

The Higher Order Component (HOC) Pattern revolves around the concept of a higher-order component, which is a function that takes a component as input and returns a new component with additional functionality. HOCs act as wrappers around components, providing them with extra props, data, or behavior.

Here's an example of a higher-order component that adds authentication functionality to a component:

function withStyles(Component) {
  return props => {
    const style = {
      padding: '0.2rem',
      margin: '1rem',
      // Merge props
      ...props.style
    }

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

// The `Button` component has a `style` prop, that shouldn't get overwritten in the HOC.
const Button = () = <button style={{ color: 'red' }}>Click me!</button>
const StyledButton = withStyles(Button)

In the withStyles function, a new functional component is created and returned. This component accepts props and applies the desired styling to the input component by merging the style prop provided with the additional styling defined within the HOC.

the Button component is a regular component that accepts a style prop. By using the withStyles HOC, a new component called StyledButton is created. The StyledButton component now has the added styling provided by the HOC. When rendered, the StyledButton component will display a button with the original style prop merged with the additional styling defined in the withStyles HOC.

This HOC pattern is useful for applying common styling or behavior to multiple components without duplicating code. It promotes reusability and modularity by separating concerns and allowing components to focus on their specific functionality while leveraging the shared capabilities provided by the HOC.

Benefits of Higher Order Component (HOC) Pattern:

  1. Reusability: HOCs promote code reusability by encapsulating common functionality that can be applied to multiple components. This eliminates code duplication and improves development efficiency.

  2. Composition: HOCs enable flexible component composition by wrapping components with additional behavior or props. This allows for the easy extension and customization of components without modifying their original implementation.

  3. Separation of Concerns: HOCs help separate concerns by extracting specific functionality into separate components. This leads to a clearer separation between core component logic and additional functionality, improving code maintainability.

  4. Cross-Cutting Concerns: HOCs are especially useful for handling cross-cutting concerns like authentication, data fetching, or logging. They provide a centralized way to apply such concerns consistently across multiple components.

  5. Code Organization: HOCs enhance code organization by centralizing shared functionality. This improves code readability and makes it easier to update and maintain the codebase.

Render Prop Pattern

The Render Props Pattern allows components to share code and data by utilizing a prop whose value is a function. This pattern promotes code reuse and provides flexibility in component composition.

Understanding the Render Props Pattern:

The Render Props Pattern revolves around the concept of passing a function as a prop to a component, which can then be invoked within the component to render dynamic content. This enables components to share data, behavior, or rendering logic with other components in a flexible and reusable manner.

Here's an example of a component that utilizes the Render Props Pattern:

import React from 'react';

const MouseTracker = ({ render }) => {
  const handleMouseMove = (event) => {
    const { clientX, clientY } = event;
    // Process mouse position
    // ...
    // Invoke the render prop with the updated data
    render(clientX, clientY);
  };

  return <div onMouseMove={handleMouseMove}>Track the mouse position</div>;
};

export default MouseTracker;

In the code snippet above, the MouseTracker component accepts a render prop, which is a function. Inside the component, the handleMouseMove function is triggered whenever the mouse moves. It then calls the render function, passing the updated data (in this case, the mouse position) as arguments.

Using the Render Props Pattern:

To utilize the Render Props Pattern, you can provide a function as a child of the component or pass it as a prop. Here's an example of how the MouseTracker component, as explained above, can be used:

import React from 'react';
import MouseTracker from './MouseTracker';

const App = () => {
  const renderMousePosition = (x, y) => {
    // Render component based on mouse position
    return (
      <div>
        Mouse Position: {x}, {y}
      </div>
    );
  };

  return (
    <div>
      <h1>Render Props Example</h1>
      <MouseTracker render={renderMousePosition} />
    </div>
  );
};

export default App;

In this example, the App component renders the MouseTracker component and provides the renderMousePosition function as the render prop. The renderMousePosition function receives the mouse position as arguments and returns the JSX that should be rendered based on that data.

Benefits of the Render Props Pattern:

  1. Code Reusability: Components implementing the Render Props Pattern can be reused across different parts of an application, enabling code reuse and reducing duplication.

  2. Flexibility: By allowing components to provide their rendering logic through a function prop, the Render Props Pattern provides flexibility in composing components and customizing their behavior based on the specific use case.

  3. Sharing Complex Logic: The Render Props Pattern allows components to share complex logic or data with child components without introducing unnecessary coupling or hierarchy.

  4. Testability: The Render Props Pattern promotes testability as components can be tested in isolation by passing different rendering functions as props.

It's important to note that while the Render Props Pattern is powerful, it can lead to more complex component hierarchies and nesting. It's essential to strike a balance between code organization and component simplicity when applying this pattern.

Hooks Pattern

The Hooks Pattern is a modern design pattern introduced in React that allows functional components to have state and utilize other React features without writing a class. Hooks provide a more concise and declarative way of managing component state and sharing code. In this section, we will explore the Hooks Pattern and demonstrate its implementation using functional components.

Understanding the Hooks Pattern:

React Hooks are a special type of function in React that serves multiple purposes:

  1. Adding state to functional components: With built-in hooks like useState, you can incorporate state management into functional components, allowing them to have their own state variables and update them when needed.

  2. Reusing stateful logic: Hooks enable you to reuse stateful logic among multiple components throughout your application. By creating custom hooks, you can encapsulate complex logic and easily share it across different components, promoting code reuse and reducing redundancy.

  3. Managing component lifecycles: React hooks, such as useEffect, allow you to manage the lifecycle of functional components. You can perform side effects, like fetching data or subscribing to events, within the hook's callback function, ensuring that these operations are performed at the appropriate times during the component's lifecycle.

In addition to the built-in hooks like useState, useEffect, and useReducer, you have the flexibility to create your own custom hooks. These custom hooks encapsulate reusable logic and provide an intuitive way to share stateful behavior across multiple components within your application. Custom hooks promote modularity and enhance the maintainability of your codebase.

Let's dive into an example that demonstrates the Hooks Pattern:

In the container/presentational pattern, we separated our stateful component logic and stateless component logic. Now, with the help of the hooks pattern, we can create a stateful function that handles states and side effects and can be used throughout the app. Let's create a custom hook that will fetch from the URL and return the obtained result.

Implementation

React knows a hook is a hook when the name starts with use. Since this hook keeps track of the hovering state, it makes sense to name it useData.

Within this hook, we have to keep track of the data state by using two build-in hooks:

  1. useState, which keeps track of whether the component is currently being hovered.

  2. useEffect, which gets executed as soon as the ref has a value. In here, we can add event listeners to the component to keep track of the hovering state.

export default function useData() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    fetch('https://house-lydiahallie.vercel.app/api/listings')
      .then((res) => res.json())
      .then((res) => setListings(res.listings));
  }, []);

  return data;
}

Now, let's use this hook inside a component DisplayData.

import useListings from '../hooks/useListings';

export default function DisplayData() {
  const data = useData();

  if (!listings) return null;

  return (
    <div>
      {data.map((item) => (
        <div key={listing.id}/>
            <p>{item.title}</p>
            <p>{item.quantity}</p>
        </div>
      ))}
    </div>
  );
}

Benefits of the Hooks Pattern:

  1. Simplified Components: Hooks simplify the process of adding state to functional components, offering a more straightforward alternative to using class components with their inherent complexity.

  2. Reusable Stateful Logic: Hooks enable the reuse of stateful logic across multiple components in the application, reducing the chances of errors and facilitating composition using plain functions.

  3. Share Non-Visual Logic: Hooks provide a convenient way to share non-visual logic without relying on patterns like Higher Order Components (HOC) or Render Props, streamlining code organization and promoting code reusability.

  4. Modern Alternative to Older Patterns: The Hooks Pattern serves as a modern alternative to older React design patterns, particularly the Presentational/Container pattern typically associated with class components. Hooks offer a more concise and efficient approach to achieve the same functionality.

Provider Pattern

The React Provider Design Pattern allows components to share data and functionality with their nested descendants, bypassing the need for props drilling. It leverages the Context API in React to create a Provider component that supplies data to consuming components within a specified scope. In this section, we will explore the React Provider Design Pattern, its benefits, and provide code examples using functional components.

Understanding the React Provider Design Pattern:

The Provider pattern addresses the challenge of passing data and functionality down multiple levels of nested components without explicitly passing props at each level. It utilizes the Context API, which provides a way to share data and functions across an entire component tree.

Let's dive into an example that demonstrates the React Provider Design Pattern:

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

// Create a context
const ThemeContext = createContext();

// Provider component
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
  };

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

// Consumer component
const ThemeToggleButton = () => {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <button onClick={toggleTheme}>
      Toggle Theme ({theme})
    </button>
  );
};

const App = () => {
  return (
    <ThemeProvider>
      <div>
        <h1>React Provider Example</h1>
        <ThemeToggleButton />
      </div>
    </ThemeProvider>
  );
};

export default App;

In this example, we create a ThemeContext using createContext, which serves as the shared context between the provider and consumer components. The ThemeProvider component acts as the provider, encapsulating the state (theme) and a function (toggleTheme) within its scope. The ThemeProvider component renders the ThemeContext.Provider component, passing the state and function as the value prop.

The ThemeToggleButton component serves as the consumer and consumes the theme and toggleTheme values from the ThemeContext using the useContext hook. It renders a button that triggers the toggleTheme function when clicked, toggling the theme between "light" and "dark".

By utilizing the React Provider Design Pattern, we can easily share data and functionality across components within a specific scope, eliminating the need for prop drilling and simplifying the communication between components.

Benefits of the Provider Pattern:

  1. Avoids Prop Drilling:: The Provider Pattern simplifies the sharing of data across components by eliminating the need for prop drilling. Data can be provided at a higher level in the component hierarchy and accessed by any nested component that subscribes to the context.

  2. Centralized State Management: The Provider Pattern enables centralized state management by allowing data to be managed in a single location. This promotes better organization and makes it easier to track and update shared data throughout the application.

  3. Flexible and Scalable Architecture: The Provider Pattern facilitates a flexible and scalable architecture, where components can easily subscribe to the relevant context and consume the provided data. This makes it easier to add or modify components without tightly coupling them to specific data sources.

Compound Pattern

The React Compound Pattern is a design pattern that combines multiple React patterns to create more complex and reusable components. It leverages the composition and separation of concerns principles to break down complex UI elements into smaller, manageable components. In this section, we will explore the React Compound Pattern, its benefits, and provide code examples using functional components.

Understanding the React Compound Pattern:

The Compound Pattern encourages the creation of reusable components by composing smaller, more specialized components together. It promotes a modular approach where each component focuses on a specific functionality or aspect of the UI.

In many applications, we encounter components that are interrelated and depend on each other through shared state and logic. Examples of such components include select dropdowns, menu items, and other interconnected UI elements. The compound component pattern enables the creation of a cohesive group of components that collaboratively accomplish a specific task.

Let's delve into an example that demonstrates the React Compound Pattern:

In our application, we have a list of squirrel images that we want to enhance by adding functionality for editing or deleting each image. To achieve this, we can implement a FlyOut component that displays a list of options when the user interacts with it. This allows the user to toggle the component and perform actions such as editing or deleting the selected image.

The FlyOut component consists of three main elements:

  1. The FlyOut wrapper: This wrapper encompasses both the toggle button and the list of menu items.

  2. The Toggle button: The toggle button is responsible for activating or deactivating the display of the List when clicked by the user.

  3. The List: The list component contains the menu items that are displayed when the toggle button is activated. These menu items provide options for the user to interact with.

By organizing the FlyOut component into these distinct elements, we create a modular and user-friendly interface that allows users to toggle and interact with the menu items effortlessly.

The Compound component pattern combined with React's Context API is an ideal approach for this scenario.

To begin, we will create the FlyOut component. This component will manage the state and return a FlyOutProvider, which will provide the toggle value to all its child components.

const FlyOutContext = createContext();

function FlyOut(props) {
  const [open, toggle] = useState(false);

  return (
    <FlyOutContext.Provider value={{ open, toggle }}>
      {props.children}
    </FlyOutContext.Provider>
  );
}

We now have a stateful FlyOut component that can pass the value of open and toggle to its children!

Let's create the Toggle component. This component simply renders the component on which the user can click in order to toggle the menu.

function Toggle() {
  const { open, toggle } = useContext(FlyOutContext);

  return (
    <div onClick={() => toggle(!open)}>
      <Icon />
    </div>
  );
}

In order to actually give Toggle access to the FlyOutContext provider, we need to render it as a child component of FlyOut! We could just simply render this as a child component. However, we can also make the Toggle component a property of the FlyOut component!

const FlyOutContext = createContext();

function FlyOut(props) {
  const [open, toggle] = useState(false);

  return (
    <FlyOutContext.Provider value={{ open, toggle }}>
      {props.children}
    </FlyOutContext.Provider>
  );
}

function Toggle() {
  const { open, toggle } = useContext(FlyOutContext);

  return (
    <div onClick={() => toggle(!open)}>
      <Icon />
    </div>
  );
}
FlyOut.Toggle = Toggle;

This means that if we ever want to use the FlyOut component in any file, we only have to import FlyOut!

import React from "react";
import { FlyOut } from "./FlyOut";

export default function FlyoutMenu() {
  return (
    <FlyOut>
      <FlyOut.Toggle />
    </FlyOut>
  );
}

Just a toggle is not enough. We also need to have a List with list items, which open and close based on the value of open.

function List({ children }) {
  const { open } = React.useContext(FlyOutContext);
  return open && <ul>{children}</ul>;
}

function Item({ children }) {
  return <li>{children}</li>;
}

The List component renders its children based on whether the value of open is true or false. Let's make List and Item a property of the FlyOut component, just like we did with the Toggle component.

const FlyOutContext = createContext();

function FlyOut(props) {
  const [open, toggle] = useState(false);

  return (
    <FlyOutContext.Provider value={{ open, toggle }}>
      {props.children}
    </FlyOutContext.Provider>
  );
}

function Toggle() {
  const { open, toggle } = useContext(FlyOutContext);

  return (
    <div onClick={() => toggle(!open)}>
      <Icon />
    </div>
  );
}

function List({ children }) {
  const { open } = useContext(FlyOutContext);
  return open && <ul>{children}</ul>;
}

function Item({ children }) {
  return <li>{children}</li>;
}

FlyOut.Toggle = Toggle;
FlyOut.List = List;
FlyOut.Item = Item;

We can now use them as properties on the FlyOut component! In this case, we want to show two options to the user: Edit and Delete. Let's create a FlyOut.List that renders two FlyOut.Item components, one for the Edit option, and one for the Delete option.

import React from "react";
import { FlyOut } from "./FlyOut";

export default function FlyoutMenu() {
  return (
    <FlyOut>
      <FlyOut.Toggle />
      <FlyOut.List>
        <FlyOut.Item>Edit</FlyOut.Item>
        <FlyOut.Item>Delete</FlyOut.Item>
      </FlyOut.List>
    </FlyOut>
  );
}

Perfect! We just created an entire FlyOut component without adding any state in the FlyOutMenu itself!

The compound pattern is great when you're building a component library. You'll often see this pattern when using UI libraries like Semantic UI.

Benefits of Compound Pattern:

  1. Simplified State Management: Compound components handle their internal state, eliminating the need for developers to manually manage state within each child component. This simplifies the implementation process, allowing us to focus on the functionality of the compound component without the overhead of state management.

  2. Single Import Convenience: Importing a compound component provides access to all the child components it encompasses. We don't need to import each individual child component separately, saving time and effort. This streamlined approach improves the developer experience and reduces the complexity of importing multiple components.

Summary

In this article, we covered six essential React design patterns: Container/Presentational, Higher Order Component (HOC), Render Props, Hooks, Provider, and Compound. These patterns help simplify component structure, enable code reuse, enhance state management, and promote modularity. By incorporating these patterns into your React applications, you can improve code organization, maintainability, and scalability. Mastering these design patterns will empower you to write cleaner, more efficient React code and create robust applications.

Connect with me

Twitter Github Linkedin