Home » Optimizing React Apps | Design Patterns

Optimizing React Apps | Design Patterns

design patterns

React design may be difficult, especially when creating huge and complicated applications. There is, however, a solution: design patterns.

Design patterns in React are vital because they allow you to build scalable, maintainable, and simple applications. They also boost developer productivity by solving common problems and encouraging code reuse.

React is a declarative, functional, and component-based library, so it’s essential to follow certain principles and patterns to take full advantage of its features and avoid unnecessary complexity.

In this article, we will discuss some of the trendiest design patterns for React. These patterns will help you organize and structure your code, making it easier to understand and modify in the future.

The patterns we will cover include:

  • The Container/Presentational pattern, which separates the logic and presentation of a component.
  • Higher-Order Components, which help to reuse component logic.
  • Render Props, which allows components to share behavior.
  • Hooks, which allow you to use state and other React features in functional components.
  • The Context API, which allows you to share data across multiple components without passing props down manually.
  • The composition model allows for efficient code reuse; thus, it’s recommended to use composition over inheritance.

Each of these patterns is like a tool in your toolbox for building React applications, and by the end of this article, you’ll have a better understanding of how and when to use them.


Let’s start and learn each pattern one by one.

1. Container/Presentational pattern:

The container-presentational pattern separates a component’s logic and state from its presentation.

It involves splitting a component into two parts:

  • A container component that handles the logic, such as fetching data and managing the state
  • A presentational component that handles the display of the data

The container component passes data and callbacks to the presentational component via props.

For example, you could have a container component that fetches a list of products from an API, and a presentational component that displays the list of products. The container component would manage the state of the products and handle any events related to the products, such as adding or deleting a product.

The presentational component would simply receive the list of products as props and display them in a list.

This pattern makes the code more modular, reusable, and easier to understand and maintain. It can be used in many different scenarios, such as creating a reusable form component or handling the state of a modal.

Here’s an example of how this pattern could be implemented in React:

//Container component
import React, { useState, useEffect } from 'react';
import PresentationalComponent from './PresentationalComponent';

function ContainerComponent() {
    const [products, setProducts] = useState([]);

    useEffect(() => {
        //fetch products from an API
        fetch('https://my-api.com/products')
            .then(res => res.json())
            .then(data => setProducts(data));
    }, []);

    const handleDeleteProduct = (id) => {
        //delete product from state
        setProducts(prevProducts => prevProducts.filter(p => p.id !== id));
    }

    return (
        <PresentationalComponent products={products} handleDeleteProduct={handleDeleteProduct}/>
    );
}

export default ContainerComponent;

//Presentational component
import React from 'react';

function PresentationalComponent({ products, handleDeleteProduct }) {
    return (
        <div>
            <h1>Product List</h1>
            <ul>
                {products.map(product => (
                    <li key={product.id}>
                        {product.name}
                        <button onClick={() => handleDeleteProduct(product.id)}>Delete</button>
                    </li>
                ))}
            </ul>
        </div>
    );
}

export default PresentationalComponent;

In this example, the ContainerComponent fetches a list of products from an API and manages the state of the products.

It also has ahandleDeleteProductfunction that deletes a product from the state. The Presentational Component receives the products andhandleDeleteProduct as props and displays them in a list.

The presentation component does not know about the state or logic of the application, it only knows how to present the data.

This example is a simple one and it only goes to show you how the Container/Presentational pattern is implemented in a real-world scenario. The pattern can be more complex, you can have multiple container components and multiple presentational components, but the main idea is the same: separate the logic and presentation.

2. Higher-Order Component (HOC) Pattern:

This pattern in React allows you to reuse component logic. A higher-order component is a function that takes a component as an argument and returns a new component with additional functionality.

HOCs are used to abstract away common functionality that is shared among multiple components, such as authentication or data fetching. Instead of duplicating the same code in multiple components, you can create a single HOC that handles the shared functionality and then use it in multiple components.

For example, you can create a HOC that handles authentication and use it in multiple components that require authentication. The HOC will handle the logic of checking if the user is authenticated and redirecting them to the login page if they are not.

The components that use the HOC do not need to know about the authentication logic, they only need to know if the user is authenticated or not.

Implementing Higher-Order Components in a React application is straightforward. Here are the steps to create a HOC:

  1. Create a new function that takes a component as an argument. This function is your HOC.
  2. Inside the HOC, use the React with function to create a new component that wraps the original component with additional functionality.
  3. Inside the HOC, you can use lifecycle methods, props, and state to add the desired functionality to the wrapped component.

Here’s an example of a HOC that adds authentication functionality to a component:

import React, { useEffect } from 'react';
import { useAuth } from './auth-context';

const withAuth = (WrappedComponent) => {
 return (props) => {
  const { isAuthenticated, login } = useAuth();

  useEffect(() => {
   if (!isAuthenticated) {
    login();
   }
  }, [isAuthenticated, login]);

  return <WrappedComponent {...props} />;
 };
};

In this example, the HOC withAuth takes a component WrappedComponent as an argument and returns a new functional component that wraps the original component with authentication functionality.

The new component checks if the user is authenticated using the useAuth hook, and calls the login function if they are not.

To use the HOC in a component, you simply import it and use it as a higher-order component.

import withAuth from './withAuth';

const MyFunctionalComponent = (props) => {
  return (
    <div>
      <h1>Welcome to MyFunctionalComponent</h1>
      <p>You are authenticated: {props.isAuthenticated ? 'Yes' : 'No'}</p>
    </div>
  );
}

export default withAuth(MyFunctionalComponent);

This way, you can use withAuth HOC in multiple functional components and it will take care of authentication and redirecting to login page if user is not authenticated. This way you can keep your component simple and focused on their specific responsibilities.

3. Render Props Pattern:

Render Props is a technique for sharing component logic. Instead of using a Higher-Order Component (HOC) to share logic between components, a component can use a Render Prop to share logic.

For example, consider a component that manages the state of a form. Instead of passing the state down to each form input component, the form component can use a Render Prop to share the state with the input components.

The form component would provide a render function that the input components could use to render their content, and the input components would provide the render function with the necessary props to render their content.

One of the key advantages of Render Props over HOCs is that it allows you to share logic between components without having to wrap them in another component, which can make your component tree more complex.

Here is an example of how you might use a Render Prop in a React application:

import React from 'react';

const Form = (props) => {
 const [state, setState] = React.useState({});
 const handleChange = (event) => {
  setState({
   ...state,
   [event.target.name]: event.target.value,
  });
 };

 return (
  <form>
   {props.render({
    state,
    handleChange,
   })}
  </form>
 );
};

const Input = (props) => {
 return (
  <>
   <label>
    {props. Label}
    <input
     type={props. Type}
     name={props.name}
     value={props.state[props.name]}
     onChange={props.handleChange}
    />
   </label>
  </>
 );
};

const App = () => {
 return (
  <Form
   render={({ state, handleChange }) => (
    <>
     <Input
      label="Username"
      type="text"
      name="username"
      state={state}
      handleChange={handleChange}
     />
     <Input
      label="Password"
      type="password"
      name="password"
      state={state}
      handleChange={handleChange}
     />
    </>
   )}
  />
 );
};

In this example, the Form component is using a Render Prop to share its state and handleChange function with the Input components, so they can use them to render their content.

Thus, Input components can be reused in various forms, without requiring knowledge of the state management. These components only need to receive the state and handleChange function as props to render their content.

4. The Hooks Pattern:

Hooks in React that allow you to use state and other React features in functional components. The Hooks API includes a set of functions that you can use to add functionality to your functional components.

One of the main use cases for Hooks is to manage the state of functional components, without the need for class components or other design patterns. Hooks also make it easier to share logic across multiple components, and to make your code more readable and easier to understand.

To implement Hooks in a React application, you need to import the useState, useEffect, and other Hooks from the React library. Then, you can use these Hooks in your functional components to add state, lifecycle methods, and other functionality.

Hooks provide a simpler and more intuitive way to share logic across multiple components, compared to other design patterns such as Higher-Order Components and Render Props. Additionally, they enhance the understanding of a component’s state and lifecycle, as all the logic resides within the component itself.

There are several built-in Hooks that come with React, including:

  1. useState: allows you to add state to functional components
  2. useEffect: allows you to handle side effects, such as fetching data or setting up subscriptions, in functional components
  3. useContext: allows you to access context in functional components
  4. useReducer: allows you to handle complex state transitions in functional components
  5. useCallback: allows you to create a memoized callback function that only updates when specific dependencies change
  6. useMemo: allows you to create a memoized value that only updates when specific dependencies change
  7. useRef: allows you to create a reference to a DOM node or a value in a functional component
  8. useImperativeHandle: allows you to customize the behavior of a child component when using refs
  9. useLayoutEffect: like useEffect but it runs synchronously after all DOM mutations
  10. useDebugValue: allows you to display a label for custom Hooks in the React DevTools.

5. Context API Pattern:

This will share data between components without having to pass props down through multiple levels of the component tree. You can create a “context” that any component, which is a descendant of the component providing the context, can access.

Use cases for the Context API include:

  1. Sharing global state: You can use the Context API to store global state, such as a user’s authentication status or application theme, which needs to be accessed by multiple components.
  2. Handling prop drilling: If you find yourself passing props down through multiple levels of components just so that a descendant component can access that data, you can use the Context API to avoid this “prop drilling.”
  3. Creating a centralized store: If you are using a state management library such as Redux or MobX, you can use the Context API to create a centralized store that can be accessed by any component in your application.

Note that using the Context API may result in the issue of prop drilling, causing your component tree to become tightly coupled. Therefore, it is advisable to use the Context API only when necessary.

Context API Comparison to other design patterns:

  1. HOCs and Context API can share data between components.
  2. HOCs wrap a component with another component while Context API uses provider-consumer relationship.
  3. HOCs offer more flexibility but are more complex while Context API is simpler but may lead to prop drilling.
  4. Render Props and Context API share data without using props.
  5. Render Props pass a function to a component while Context API uses provider-consumer relationship.
  6. Context API is simpler but Render Props may be more flexible.
  7. State management libraries like Redux and MobX are more powerful than Context API.
  8. State management libraries have middleware and tools for complex state management.

You should avoid using the Context API if you can use state management libraries, as they are more powerful and can handle complex state management more efficiently.

Here’s an example of how to use the Context API with functional components:

const MyContext = React.createContext();

function MyProvider({ children }) {
 const [state, setState] = useState({});

 return (
  <MyContext.Provider value={{ state, setState }}>
   {children}
  </MyContext.Provider>
 );
}

function MyConsumer() {
 const context = useContext(MyContext);
 const { state, setState } = context;

 return (
  <div>
   <p>The state is: {state}</p>
   <button onClick={() => setState({ ...state, newData: 'Hello World!' })}>
    Update State
   </button>
  </div>
 );
}

function App() {
 return (
  <MyProvider>
   <MyConsumer />
  </MyProvider>
 );
}
  1. First, we create a context object by calling the React.createContext() method. This will return an object with a Provider and a Consumer component.
  2. Next, we create a provider component that will hold the state that we want to share. This component should wrap the components that need access to the state.
  3. To access the state from a consumer component, we use the useContext() hook and pass in our context object.
  4. Finally, we use the provider component to wrap the consumer component, which will give the consumer component access to the state.

This is a basic example; it can be more complex when you have multiple consumer and provider components.

6. Composition Pattern:

React has a strong composition model which allows for the reuse of code between components without the need for inheritance.

It is recommended to use composition over inheritance when working with React.

It is advisable for these types of components to utilize the children prop to directly insert child elements into their output.

Here is an example of how you might use a composition pattern in a React application:

function FancyBorder(props) {
    return (
        <div className={"FancyBorder FancyBorder-" + props.color}>
            {props.children}
        </div>
    );
}

function WelcomeDialog() {
    return (
        <FancyBorder color="blue">
            <h1 className="Dialog-title">Welcome</h1>
            <p className="Dialog-message">Thank you for visiting our spacecraft!</p>
        </FancyBorder>
    );
}

ReactDOM.render(<WelcomeDialog />, document.getElementById("root"));

Elements within the <FancyBorder> tags are passed as the children prop to the FancyBorder component. Since FancyBorder renders {props.children} inside a <div>, the child elements are visible in the final rendered output.


In conclusion, we’ve covered some of the trendiest design patterns in React, including the Container/Presentational pattern, Higher-Order Components, Render Props, Hooks, and the Context API. Each of these patterns can help make your React code more efficient and organized.

These patterns provide solutions to common problems developers face when building React applications, and by using them, you can focus on creating unique features that will set your application apart.

🤓 Remember, design patterns are like a secret society of spells for your code. They’re not just for wizards and witches, they’re for developers too!

🧙‍♂️ So next time you’re building a React application, don’t be afraid to use some of these patterns to make your code clean, organized, and efficient.

🚀 And before you know it, your application will be soaring through the skies like a superhero!

🎉 So cheers to building better React applications with design patterns! 🍻

P.S. if you’re feeling adventurous, you can always try combining multiple patterns to create your own super-pattern! 😎


Additional resources for learning more about Design Patterns:

These resources offer a variety of perspectives and approaches to understanding and implementing design patterns, and can be a great starting point for anyone looking to deepen their knowledge in this area.

2 thoughts on “Optimizing React Apps | Design Patterns

Leave a Reply

Your email address will not be published. Required fields are marked *