Lawrence Eagles Senior full-stack developer, writer, and instructor.

A guide to React design patterns

10 min read 3048

A Guide To React Design Patterns

Editor’s note: This React components guide was last updated on 21 December 2022 to add information about the render props pattern and state reducer pattern. Check out this article for more information on React Hooks.

Design patterns are solution templates for 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.

React components design patterns

In this section, we will look at the top React component design patterns. 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.

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 the 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;
};

Render props pattern

In the previous section, we saw how the HOC can make rousing props and shared logic in a convenient way. In this section, we’ll explore yet another way to make React components reusable across our application by implementing the Render props pattern.

Imagine that we have a Paragraph component that renders anything we pass to it. The component’s primary purpose would be to render the value we pass to it. We can use the code below to achieve this:

<Paragraph render={() => <p>This is a rendered prop.</p>}>

To get the value of the render prop, we can invoke it like this:

const Paragraph = props => props.render()

Ideally, this means that Paragraph is a component that receives a render prop and returns a JSX component. Looks simple right?

Now let’s take a look at a more relatable scenario. Imagine that we had a TextInput whose value we want to share with two components. We can use Render props to handle it.

Wait, isn’t the state of the TextInput supposed to be in the parent component?

Yes, you’re right, but in larger applications, it’s often difficult to perform state lifting:

import { useState } from "react";
const TextInput = (props) => {
  const [value, setValue] = useState("");
  return (
    <>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Type text..."
      />
      {props.children(value)}
    </>
  );
};
export default TextInput;

The input component, like every other React component, has the children prop, so here we’re leveraging it to allow for both components to access its value. We can create the two-component like this:

const Comp1 = ({ value }) => <p>{value}</p>;
const Comp2 = ({ value }) => <p>{value}</p>;

Then use them like this:

 <TextInput>
          {(value) => (
            <>
              <Comp1 value={value} />
              <Comp2 value={value} />
            </>
          )}
        </TextInput>

Going forward, Comp1 and Comp2 will maintain the same value as whatever the value of TextInput is.



State reducer pattern

The state reducer pattern has gotten really popular since the release of react hooks. It has grown to be a tradition for various codebases in production, especially because of its abstraction of Redux workflow using the useReducer hook.

In this section, we’ll explore how to use the state reducer pattern to build reusable React applications. The simplest way to demolish the use of a state reducer pattern is to create a custom helper hook. In the section, we’ll create a useToggle hook for toggling component states in our application.

To begin, let’s create a type for our reducer:

const toggleActionTypes = {
  toggle: "TOGGLE",
};

Create the toggleReducer:

const toggleReducer = (state, action) => {
  switch (action.type) {
    case toggleActionTypes.toggle:
      return { on: !state.on };
    default:
      throw new Error(`Undefined type: ${action.type}`);
  }
};

Then, create the hook (useToggle):

const useToggle = ({ reducer = toggleReducer } = {}) => {
  const [{ on }, dispatch] = useReducer(reducer, { on: false });
  const toggle = () => dispatch({ type: toggleActionTypes.toggle });
  return [on, toggle];
};

We can then use it in a component like below:

const Toggle = () => {
  const [on, toggle] = useToggle({
    reducer(currentState, action) {
      const updates = toggleReducer(currentState, action);
      return updates;
    },
  });
  return (
    <div>
      <button onClick={toggle}>{on ? "Off" : "On"}</button>
    </div>
  );
};
export default Toggle;

Clicking on the button will toggle it’s on and off state. We’ve successfully made it easy for users to hook into every update that occurs in our useToggle reducer.

The provider pattern

The provider pattern in React is 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
)

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

By default, React 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.


More great articles from LogRocket:


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>
  );
}

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>

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:

First, scaffold a new React app:

yarn create react-app Accordion

cd Accordion

yarn start

Then, install dependencies:

yarn add styled-components

Next, 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;

Then, 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;
}
;

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.

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, because 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 aim to separate concerns and structure our codes in a way that is easy to understand.

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

In some cases where they need to hold a 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>
  );
};

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
  }
}

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, because 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;

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

  • Working with the 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;

In advanced 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 expertise of all the developers who created and reviewed these patterns.

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

Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    LogRocket.init('app/id');
    Add to your HTML:

    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now
Lawrence Eagles Senior full-stack developer, writer, and instructor.

2 Replies to “A guide to React design patterns”

  1. This feels like an article from 2019… l can’t recall the last time I’ve used a HOC

Leave a Reply