DEV Community

Cover image for React component design patterns for 2022
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

React component design patterns for 2022

Written by Lawrence Eagles✏️

Introduction

Design patterns are solution templates to common software development problems. In React, they are proven methods to solve common problems experienced by React developers.

As the React API evolves, new patterns emerge, and developers often favor them over older patterns. In this article, we will learn about some useful React design patterns in 2022. Here’s what we’ll cover:

Let’s get started in the next section.

2022 React components design patterns

In this section, we will look at the top React component design patterns for 2022. This list includes some of the most popular React design patterns that are efficient for cross-cutting concerns, global data sharing (without prop drilling), the separation of concerns such as complex stateful logic from other component parts, and more.

Below are the patterns:

The higher-order component pattern

The higher-order component, or HOC pattern, is an advanced React pattern used for reusing component logic across our application. The HOC pattern is useful for cross-cutting concerns — features that require sharing of component logic across our application. Examples of these features are authorization, logging, and data retrieval.

HOCs are not part of the core React API, but they arise from the compositional nature of React functional components, which are JavaScript functions.

A high-order component is akin to a JavaScript higher-order function; they are pure functions with zero side effects. And like higher-order functions in JavaScript, HOCs act like a decorator function.

In React, a higher-order component is structured as seen below:

import React, {Component} from 'react';

const higherOrderComponent = (DecoratedComponent) => {
  class HOC extends Component {
    render() {
      return <DecoratedComponent />;
    }
  }
  return HOC;
};
Enter fullscreen mode Exit fullscreen mode

The provider pattern

The provider pattern in React is an advanced pattern used to share global data across multiple components in the React component tree.

The provider pattern involves a Provider component that holds global data and shares this data down the component tree in the application using a Consumer component or a custom Hook.

The provider pattern is not unique to React; libraries like React-Redux and MobX implement the provider pattern, too.

The code below shows the setup of the provider pattern for React-Redux:

import React from 'react'
import ReactDOM from 'react-dom'

import { Provider } from 'react-redux'
import store from './store'

import App from './App'

const rootElement = document.getElementById('root')
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)
Enter fullscreen mode Exit fullscreen mode

In React, the provider pattern is implemented in the React context API.

React by default supports a unilateral downward flow of data from a parent component to its children. Consequently, to pass data to a child component located deep in the component tree, we will have to explicitly pass props through each level of the component tree — this process is called prop drilling.

The React context API uses the provider pattern to solve this problem. Thus it enables us to share data across the React components tree without prop drilling.

To use the Context API, we first need to create a context object using React.createContext. The context object comes with a Provider component that accepts a value: the global data. The context object also has a Consumer component that subscribes to the Provider component for context changes. The Consumer component then provides the latest context value props to children.

Below demonstrates a typical use case of the React context API:

import { createContext } from "react";
const LanguageContext = createContext({});
function GreetUser() {
  return (
    <LanguageContext.Consumer>
      {({ lang }) => (
        <p>Hello, Kindly select your language. Default is {lang}</p>
      )}
    </LanguageContext.Consumer>
  );
}
export default function App() {
  return (
    <LanguageContext.Provider value={{ lang: "EN-US" }}>
      <h1>Welcome</h1>
      <GreetUser />
    </LanguageContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

The React Context API is used in implementing features such as the current authenticated user, theme, or preferred language where global data is shared across a tree of components.

N.B., React also provides a more direct API — the useContext Hook — for subscribing to the current context value instead of using the Consumer component.

The compound components pattern

Compound components is an advanced React container pattern that provides a simple and efficient way for multiple components to share states and handle logic — working together.

The compound components pattern provides an expressive and flexible API for communication between a parent component and its children. Also, the compound components pattern enables a parent component to interact and share state with its children implicitly, which makes it suitable for building declarative UI.

Two good examples are the select and options HTML elements. Both select and options HTML elements work in tandem to provide a dropdown form field.

Consider the code below:

<select>
  <option value="javaScript">JavaScript</option>
  <option value="python">Python</option>
  <option value="java">Java</option>
</select>
Enter fullscreen mode Exit fullscreen mode

In the code above, the select element manages and shares its state implicitly with the options elements. Consequently, although there is no explicit state declaration, the select element knows what option the user selects.

The compound component pattern is useful in building complex React components such as a switch, tab switcher, accordion, dropdowns, tag list, etc. It can be implemented either by using the context API or the React.cloneElement API.

In this section, we will learn more about the compound components pattern by building an accordion. We will implement our compound components pattern with the context API. Simply follow the steps below:

  1. Scaffold a new React app:

    yarn create react-app Accordion
    
    cd Accordion
    
    yarn start
    
  2. Install dependencies:

    yarn add styled-components
    
  3. Add dummy data: In the src directory, create a data folder and add the code below:

    const faqData = [
    
    {
    
    id: 1,
    
    header: "What is LogRocket?",
    
    body:
    
    "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
    
    },
    
    {
    
    id: 2,
    
    header: "LogRocket pricing?",
    
    body:
    
    "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
    
    },
    
    {
    
    id: 3,
    
    header: "Where can I Find the Doc?",
    
    body:
    
    "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
    
    },
    
    {
    
    id: 4,
    
    header: "How do I cancel my subscription?",
    
    body:
    
    "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
    
    },
    
    {
    
    id: 5,
    
    header: "What are LogRocket features?",
    
    body:
    
    "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
    
    }
    
    ];
    
    export default faqData;
    
  4. Create components and add styles:In the src directory, create a components folder, an Accordion.js file, and an Accordion.styles.js file. Now we will create our styles using style-components. Add the following code to the Accordion.styles.js file:

    import styled from "styled-components";
    
    export const Container = styled.div `display: flex;
    background: #6867ac;
    border-bottom: 8px solid #ffbcd1;
    font-family: "Inter", sans-serif;` ; export const Wrapper = styled.div `margin-bottom: 40px;` ; export const Inner = styled.div `display: flex;
    padding: 70px 45px;
    flex-direction: column;
    max-width: 815px;
    margin: auto;` ; export const Title = styled.h1 `font-size: 33px;
    line-height: 1.1;
    margin-top: 0;
    margin-bottom: 8px;
    color: white;
    text-align: center;` ; export const Item = styled.div `color: white;
    margin: auto;
    margin-bottom: 10px;
    max-width: 728px;
    width: 100%;
    &:first-of-type {
    margin-top: 3em;
    }
    &:last-of-type {
    margin-bottom: 0;
    }` ; export const Header = styled.div `display: flex;
    flex-direction: space-between;
    cursor: pointer;
    border: 1px solid #ce7bb0;
    border-radius: 8px;
    box-shadow: #ce7bb0;
    margin-bottom: 1px;
    font-size: 22px;
    font-weight: normal;
    background: #ce7bb0;
    padding: 0.8em 1.2em 0.8em 1.2em;
    user-select: none;
    align-items: center;` ; export const Body = styled.div `font-size: 18px;
    font-weight: normal;
    line-height: normal;
    background: #ce7bb0;
    margin: 0.5rem;
    border-radius: 8px;
    box-shadow: #ce7bb0;
    white-space: pre-wrap;
    user-select: none;
    overflow: hidden;
    &.open {
    max-height: 0;
    overflow: hidden;
    }
    span {
    display: block;
    padding: 0.8em 2.2em 0.8em 1.2em;
    }` ;
    
  5. Next, add the following code to the Accordion.js file:

    import React, { useState, useContext, createContext } from "react";
    import { Container, Inner, Item, Body, Wrapper, Title, Header
    } from "./Accordion.styles";
    
    const ToggleContext = createContext();
    export default function Accordion({ children, ...restProps }) {
      return (
        <Container {...restProps}>
          <Inner>{children}</Inner>
        </Container>
      );
    }
    
    Accordion.Title = function AccordionTitle({ children, ...restProps }) {
      return <Title {...restProps}>{children}</Title>;
    };
    
    Accordion.Wrapper = function AccordionWrapper({ children, ...restProps }) {
      return <Wrapper {...restProps}>{children}</Wrapper>;
    };
    
    Accordion.Item = function AccordionItem({ children, ...restProps }) {
      const [toggleShow, setToggleShow] = useState(true);
      const toggleIsShown = (isShown) => setToggleShow(!isShown);
      return (
        <ToggleContext.Provider value={{ toggleShow, toggleIsShown }}>
          <Item {...restProps}>{children}</Item>
        </ToggleContext.Provider>
      );
    };
    
    Accordion.ItemHeader = function AccordionHeader({ children, ...restProps }) {
      const { toggleShow, toggleIsShown } = useContext(ToggleContext);
      return (
        <Header onClick={() => toggleIsShown(toggleShow)} {...restProps}>
          {children}
        </Header>
      );
    };
    
    Accordion.Body = function AccordionBody({ children, ...restProps }) {
      const { toggleShow } = useContext(ToggleContext);
      return (
        <Body className={toggleShow ? "open" : ""} {...restProps}>
          <span>{children}</span>
        </Body>
      );
    };
    

    In the code above, the ToggleContext context object holds our toggleShow state and provides this state to all Accordion children via the ToggleContext.Provider. Also, we created and attached new components to the Accordion component by using the JSX dot notation.

  6. Finally, update the App.js with the following code:

    import React from "react";
    import Accordion from "./components/Accordion";
    import faqData from "./data";
    export default function App() {
      return (
        <Accordion>
          <Accordion.Title>LogRocket FAQ</Accordion.Title>
          <Accordion.Wrapper>
            {faqData.map((item) => (
              <Accordion.Item key={item.id}
                <Accordion.ItemHeader>{item.header}</Accordion.ItemHeader>
                <Accordion.Body>{item.body}</Accordion.Body>
              </Accordion.Item>
            ))}
          </Accordion.Wrapper>
        </Accordion>
      );
    }
    

You can see the accordion in action here.

The presentational and container component patterns

These terms were originally coined by Dan Abramov. However, he does not promote these ideas anymore.

Both the presentational and container patterns are useful because they help us separate concerns e.g., complex stateful logic, from other aspects of a component.

However, since React Hooks enable us to separate concerns without any arbitrary division, the Hooks pattern is recommended instead of the presentational and container component pattern. But depending on your use case, the presentational and container patterns may still come in handy.

These patterns are aimed to separate concerns and structure our codes in a way that is easy to understand and reason with.

The presentational components are stateless functional components that are only concerned with rendering data to the view. And they have no dependencies with the other parts of the application.

In some cases where they need to hold state related to the view, they can be implemented with React class components.

An example of a presentational component is a component that renders a list:

const usersList = ({users}) => {
  return (
  <ul>
      {users.map((user) => (
      <li key={user.id}>
          {user.username}
      </li>
      ))}
  </ul>
  );
};
Enter fullscreen mode Exit fullscreen mode

Container components are useful class components that keep track of their internal state and life cycle. They also contain presentational components and data-fetching logic.

An example of a container component is shown below:

class Users extends React.Component {
  state = {
    users: []
  };

  componentDidMount() {
    this.fetchUsers();
  }

  render() {
    return (); // ... jsx code with presentation component
  }
}
Enter fullscreen mode Exit fullscreen mode

The Hooks pattern

The React Hooks APIs were introduced to React 16.8 and have revolutionized how we build React components.

The React Hooks API gives React functional components a simple and direct way to access common React features such as props, state, context, refs, and lifecycle.

The result of this is that functional components do not have to be dumb components anymore as they can use state, hook into a component lifecycle, perform side effects, and more from a functional component. These features were originally only supported by class components.

Although patterns such as the presentational and container component patterns enable us to separate concerns, containers often result in “giant components”: components with a huge logic split across several lifecycle methods. And giant components can be hard to read and maintain.

Also, since containers are classes, they are not easily composed. And when working with containers, we are also faced with other class-related problems such as autobinding and working the this.

By supercharging functional components with the ability to track internal state, access component lifecycle, and other class-related features, the Hooks patterns solve the class-related problems mentioned above. As pure JavaScript functions, React functional components are composable and eliminate the hassle of working with this keyword.

Consider the code below:

import React, { Component } from "react";
class Profile extends Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: false,
      user: {}
    };
  }
  componentDidMount() {
    this.subscribeToOnlineStatus(this.props.id);
    this.updateProfile(this.props.id);
  }
  componentDidUpdate(prevProps) {
    // compariation hell.
    if (prevProps.id !== this.props.id) {
      this.updateProfile(this.props.id);
    }
  }
  componentWillUnmount() {
    this.unSubscribeToOnlineStatus(this.props.id);
  }
  subscribeToOnlineStatus() {
    // subscribe logic
  }
  unSubscribeToOnlineStatus() {
    // unscubscribe logic
  }
  fetchUser(id) {
    // fetch users logic here
  }
  async updateProfile(id) {
    this.setState({ loading: true });
    // fetch users data
    await this.fetchUser(id);
    this.setState({ loading: false });
  }
  render() {
     // ... some jsx
  }
}
export default Profile;
Enter fullscreen mode Exit fullscreen mode

From the container above, we can point out three challenges:

  • Working with constructor and calling super() before we can set state. Although this has been solved with the introduction of class fields in JavaScript, Hooks still provide a simpler API
  • Working with this
  • Repeating related logic across lifecycle methods

Hooks solves these problems by providing a cleaner and leaner API. Now we can refactor our Profile component as seen below:

import React, { useState, useEffect } from "react";
function Profile({ id }) {
  const [loading, setLoading] = useState(false);
  const [user, setUser] = useState({});

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    updateProfile(id);
    subscribeToOnlineStatus(id);
    return () => {
      unSubscribeToOnlineStatus(id);
    };
  }, [id]);

  const subscribeToOnlineStatus = () => {
    // subscribe logic
  };

  const unSubscribeToOnlineStatus = () => {
    // unsubscribe logic
  };

  const fetchUser = (id) => {
    // fetch user logic here
  };

  const updateProfile = async (id) => {
    setLoading(true);
    // fetch user data
    await fetchUser(id);
    setLoading(false);
  };

  return; // ... jsx logic
}
export default Profile;
Enter fullscreen mode Exit fullscreen mode

In advance cases, the Hooks pattern promotes code reusability by enabling us to create custom reusable hooks. And you can learn more about this in our previous article.

Conclusion

In this article, we learned about some useful design patterns in 2022. Design patterns are great because they enable us to leverage the experience and expertise of all the developers who created and reviewed these patterns.

Consequently, they can cut development time since we are leveraging proving solution schemes and improving software quality in the process.


Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — start monitoring for free.

Top comments (1)

Collapse
 
timreach profile image
timreach

"For 2022" - yet you're still acting like hooks are new and using classes for most examples...