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;
};
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
)
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>
);
}
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:
-
Scaffold a new React app:
yarn create react-app Accordion cd Accordion yarn start
-
Install dependencies:
yarn add styled-components
-
Add dummy data: In the
src
directory, create adata
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;
-
Create components and add styles:In the
src
directory, create acomponents
folder, anAccordion.js
file, and anAccordion.styles.js
file. Now we will create our styles using style-components. Add the following code to theAccordion.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 ourtoggleShow
state and provides this state to allAccordion
children
via theToggleContext.Provider
. Also, we created and attached new components to theAccordion
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, 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>
);
};
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, 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;
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;
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 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)
"For 2022" - yet you're still acting like hooks are new and using classes for most examples...