Notes on React Design Patterns

react web application design patterns

Design pattern provide you with implementation guidelines to avoid / address common problems, organize your code in better way and make it more maintainable and extendable. Below listed are some of the common design patterns that you can use in your react projects.

Container and Presentation Components

This pattern enables you to separate the business logic from the presentation logic. This follows the separation of concerns principle.

A lot of times you will be fetching certain data using an api call and then rendering that in your app. These are the scenarios where you can use the container and presentation component pattern.

You can keep the logic to fetch the data in a container component and you can keep the logic to show this data in app in another component viz presentation component.

This design pattern is also known as the smart and dumb components pattern.

Here's an example

Container Component or smart component

// UserContainer.js

import React, { useState, useEffect } from 'react';
import UserPresentation from './UserPresentation';

const UserContainer = () => {
  // State for managing data
  const [userData, setUserData] = useState([]);

  // Simulate fetching data from an API
  useEffect(() => {
    const fetchData = async () => {
      try {
        // Fetch user data from an API
        const response = await fetch(
          'https://jsonplaceholder.typicode.com/users'
        );
        const data = await response.json();
        setUserData(data);
      } catch (error) {
        console.error('Error fetching user data:', error);
      }
    };

    fetchData();
  }, []);

  // Logic for handling user interactions or additional data manipulation can go here

  return <UserPresentation userData={userData} />;
};

export default UserContainer;

Presentation Component

// UserPresentation.js

import React from 'react';

const UserPresentation = ({ userData }) => (
  <div>
    <h1>User List</h1>
    <ul>
      {userData.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  </div>
);

export default UserPresentation;

In this example:

  • UserContainer is responsible for fetching and managing the user data, as well as any additional logic related to user interactions or data manipulation. It uses the useState and useEffect hooks to manage the state and fetch data asynchronously.

  • UserPresentation is a presentational component that receives the userData as a prop and is responsible for rendering the UI. It doesn't have any logic related to state management or data fetching.

By organizing components in this way, it becomes easier to maintain and understand the codebase. The separation of concerns makes it clear which components are responsible for state and logic and which ones are focused on rendering UI. Additionally, it facilitates reusability, as the presentational components can be reused in different parts of the application.

Higher Order Components

Higher order components take another component as an input and return a new component with enhanced functionality and utilities.

Here's a simple example of an HOC that adds a loading spinner to a component while data is being fetched:

// withLoadingHOC.js

import React, { Component } from 'react';

const withLoadingHOC = (WrappedComponent) => {
  return class WithLoading extends Component {
    state = {
      isLoading: true
    };

    componentDidMount() {
      // Simulate data fetching
      setTimeout(() => {
        this.setState({ isLoading: false });
      }, 2000);
    }

    render() {
      return this.state.isLoading ? (
        <div>Loading...</div>
      ) : (
        <WrappedComponent {...this.props} />
      );
    }
  };
};

export default withLoadingHOC;

Usage:

// ExampleComponent.js

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

const ExampleComponent = () => (
  <div>
    <h1>Hello, World!</h1>
    {/* Additional component content */}
  </div>
);

export default withLoadingHOC(ExampleComponent);

In this example, withLoadingHOC is a higher-order component that adds a loading spinner to any component passed to it. This way, the loading logic is encapsulated and can be easily reused across different components in the application.

Provider Pattern

One of the common issues which I have seen in react codebase is prop drilling. This is where the props are transferred in a component tree from parent to child explicitly at each level.

Now I have written a whole article on prop drilling and how you can avoid it. But few disadvantages of prop drilling are unnecessary duplication of code, cascaded changes, low maintainability, decreases reusability, difficult to debug etc.

The way you avoid prop drilling is by using Context api to build data providers. Data providers can expose data which can be accessed across the component tree without the need for passing the props explicitly.

Read more about how to do this in my article here