Editor’s note: This guide to React design patterns was last reviewed for accuracy by Isaac Okoro on 12 April 2024. The article was also updated to add four more design patterns, covering prop combination, controlled components, forwardRefs
, and conditional rendering. It was previously updated to include 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 2024.
Let’s 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, 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, such as 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. Also 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; };
In the previous section, we saw how the HOC pattern can make rousing props and shared logic more convenient. 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 common 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 components like this:
const Comp1 = ({ value }) => <p>{value}</p>; const Comp2 = ({ value }) => <p>{value}</p>;
Then, we can 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.
The state reducer pattern saw a surge in popularity after the release of React Hooks. It has grown to be a tradition for various codebases in production, especially because of its abstraction of the 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. So, let’s 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 useToggle
Hook:
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 its 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 in React is used to share global data across multiple components in the React component tree. This 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.
Note that 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.
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. It also has a Consumer
component that subscribes to the Provider
component for context changes and 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.
Note that React also provides a more direct API — the useContext
Hook — for subscribing to the current context value instead of using the Consumer
component.
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, and more. It can be implemented either by using the Context API or the React.cloneElement
function.
In this section, we will learn more about the compound components pattern by building an Accordion
component. 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;
Next, we’ll 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 styled-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.
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 lifecycle. 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 React Hooks APIs were introduced to React 16.8 and have revolutionized how we build React components.
React Hooks give 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. These 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’re also faced with other class-related problems such as autobinding and working the this
keyword.
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:
super()
before we can set state. Although this has been solved with the introduction of class fields in JavaScript, Hooks still provide a simpler APIthis
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. You can learn more about this in our previous article.
Props are used to pass data from one component to another. The prop combination pattern groups related props into a single object. This object is then passed as a single prop to a component.
Some benefits of this pattern include reduction of boilerplate code, improving code readability and maintainability.
Let’s see how we can implement this pattern below:
import React from 'react'; function Button({ style, onClick, children }) { const buttonStyle = { backgroundColor: style.color || 'blue', fontSize: style.size || '16px', fontWeight: style.weight || 'bold' }; return ( <button style={buttonStyle} onClick={onClick}> {children} </button> ); } export default Button;
In the code block above, we have a Button
component. Now, we could have separate props for the size, color, and font weight of the button. But using the props combination pattern, we can combine these props into a single prop object called style
.
We can then pass our single style
prop object to any component that uses the Button
component, allowing that component to customize the Button
component, as shown below:
import React from 'react'; import Button from './Button'; function App() { return ( <div> <Button style={{ color: 'red', weight: 'light', size: '20px' }} onClick={() => console.log('Button clicked')}> Submit </Button> </div> ); } export default App;
The controlled component pattern helps with managing form inputs by establishing a clear one-way data flow between the form input and its state. The form input gets it’s state through props and then uses a callback like onChange
to notify the state if any changes happen.
This pattern ensures that components act predictably and reliably unlike uncontrolled components. An example of this pattern is shown below:
import React, { useState } from "react"; function MyForm() { const [inputValue, setInputValue] = useState(""); const handleChange = (event) => { setInputValue(event.target.value); }; return ( <form> <input type="text" value={inputValue} onChange={handleChange} /> </form> ); }
In the code block above, we created an input element and the created a state value for monitoring the state which we passed to the input as props. We then used the onChange
callback to monitor any changes and then update the state of the input. This pattern ensures that the input field works predictably every time
forwardRef
methodThe ref
prop is typically used to obtain a reference to a DOM element. The forwardRef
design pattern becomes very handy when you need to forward a reference to a custom component or from a parent component down to a child component.
This design pattern allows the parent component to access and interact with the underlying DOM element or instance of the child component. Let’s see an example of how to use forwardRef
in React:
import React from 'react'; const ChildComponent = React.forwardRef((props, ref) => { <input ref={ref} {...props} /> }); const ParentComponent = () => { const childRef = React.useRef(); return <ChildComponent ref={childRef} />; };
The code block above shows a Child
component wrapped with forwardRef
. This allows the ref
to be passed and accessed by the ParentComponent
.
Conditional rendering involves dynamically displaying different UI elements based on certain conditions. This pattern is very useful when building applications that display different information depending on application state, user interactions and various other factors.
There are multiple ways to implement conditional rendering in React. The first and most basic way is using the if
statement, as shown below:
function showLoggenInUser(props) { if (props.isAuthenticated) { return <p>Welcome back, {props.username}!</p>; } else { return <p>Please sign in.</p>; } }
Another way to conditionally render components is by using the ternary operator. The ternary operator is shorthand for an if
statement and can be used directly within JSX, as shown below:
functionshowLoggenInUser(props) { return props.isAuthenticated ? ( <p>Welcome back, {props.username}!</p> ) : ( <p>Please sign in.</p> ); }
Another way is using the logical AND
operator as shown below:
function showLoggenInUser(props) { return props.isAuthenticated && <p>Welcome back, {props.username}!</p>; }
If you’re interested in learning more, we explore nine methods of React conditional rendering in this article, with examples to help you better understand when and how to use this method.
In this article, we learned about some useful design patterns in 2024. 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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ 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>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
2 Replies to "A guide to React design patterns"
This feels like an article from 2019… l can’t recall the last time I’ve used a HOC
Most of these won’t really apply unless you’re writing a library. For your own app, use redux and thunks and hooks.