TL;DR: The 5 essential React design patterns are the HOC(Higher Order Component), Provider, Container/Presentational, Compound, and Hooks patterns. They optimize your app development process. This blog contains descriptions and examples of these design patterns.
React provides a variety of exceptional features and rich design patterns to ease the development process. Developers can use React component design patterns to reduce development time and coding efforts. Additionally, these patterns enable React developers to build extensive applications that offer greater results and higher performance.
This article will introduce you to five essential React component design patterns, providing examples to assist you in optimizing your React apps.
As your app scales, scenarios arise in which you need to share the same component logic across multiple components. This is what the HOC pattern lets you do.
An HOC is a pure JavaScript function that accepts one component as an argument and returns another after injecting or adding additional data and functionality. It essentially acts as a JavaScript decorator function. The fundamental concept of HOCs aligns with React’s nature, which favors composition over inheritance.
For instance, let’s consider a scenario where we need to style numerous app components uniformly. We can bypass repeatedly constructing a style object locally by implementing a HOC that applies the styles to the component passed into it.
import React from 'react'; // HOC function decoratedComponent(WrappedComponent) { return props => { const style = { padding: '5px', margin: '2px' }; return <WrappedComponent style={style} {...props} />; }; } const Button = ({ style }) => <button style={{ ...style, color: 'yellow' }}>This is a button.</button>; const Text = ({ style }) => <p style={style}>This is text.</p>; const DecoratedButton = decoratedComponent(Button); const DecoratedText = decoratedComponent(Text); function App() { return ( <> <DecoratedButton /> <DecoratedText /> </> ); } export default App;
In the previous code example, we’ve modified the Button and Text components to produce DecoratedButton and DecoratedText, respectively. Now both components inherit the style added by the decoratedComponent HOC. Since the Button component already has a prop named style, the HOC will override it and append the new prop.
In complex React apps, the challenge of making data accessible to multiple components often arises. While props can be used to pass data, accessing prop values across all components can become cumbersome, leading to prop drilling.
The Provider pattern, leveraging the React Context API and, in some cases, Redux, offers a solution to this challenge. This pattern allows developers to store data in a centralized area, known as a React context object or a Redux store, eliminating the need for prop drilling.
React-Redux utilizes the Provider pattern at the top level of an app to grant access to the Redux store for all components. Refer to the following code example for how to set it up.
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 cases where Redux might be overkill, React’s Context API can be used. For instance, if an App component has a dataset that needs to be accessed by List, PageHeader, and Text components deep in the component tree, the Context API can bypass prop drilling.
Refer to the following code example to understand how to create and provide a context.
import React, { createContext } from 'react'; import SideMenu from './SideMenu'; import Page from './Page'; const DataContext = createContext(); function App() { const data = { list: ['Item 1', 'Item 2', 'Item 3'], text: 'Hello World', header: 'WriterGate' }; // Define your data structure here. return ( <DataContext.Provider value={data}> <SideMenu/> <Page/> </DataContext.Provider> ); }
Components can access the data using the useContext hook, which allows both reading and writing data to the context object.
Refer to the following code example.
import React, { useContext } from 'react'; const SideMenu = () => <List/> const Page = () => <div><PageHeader/><Content/></div> function List() { const data = useContext(DataContext); return <span>{data.list}</span>; } function Text() { const data = useContext(DataContext); return <h1>{data.text}</h1>; } function PageHeader() { const data = useContext(DataContext); return <div>{data.header}</div>; } const Content = () => { const data = useContext(DataContext); return <div><Text/></div>; }
The Container/Presentational pattern in React offers one approach to achieving separation of concerns, effectively segregating the view from the app logic. Ideally, we need to implement concern separation by splitting this procedure into two parts.
These components focus on how data is presented to the user. They receive data via props and are responsible for rendering it in a visually pleasing manner, typically with styles, without modifying the data.
Consider the following example, which displays food images fetched from an API. To accomplish this, we implement a functional component that receives data via props and renders it accordingly.
import React from "react"; export default function FoodImages({ foods }) { return foods.map((food, i) => <img src={food} key={i} alt="Food" />); }
In this code example, the FoodImages component acts as a presentational component. Presentational components remain stateless unless they require a React state for UI rendering. The data received isn’t altered. Instead, it’s retrieved from the corresponding container component.
These components focus on determining what data is presented to the user. Their primary role is to pass data to presentational components. Container components typically do not render components other than the presentational ones associated with their data. Container components normally don’t have any styling, as their responsibility lies in managing state and lifecycle methods rather than rendering.
Refer to the following code example of a container component that fetches images from an external API and passes them to the presentational component (FoodImages).
import React from "react"; import FoodImages from "./FoodImages"; export default class FoodImagesContainer extends React.Component { constructor() { super(); this.state = { foods: [] }; } componentDidMount() { fetch("http://localhost:4200/api/food/images/random/6") .then(res => res.json()) .then(({ message }) => this.setState({ foods: message })); } render() { return <FoodImages foods={this.state.foods} />; } }
Compound components represent an advanced React component pattern that allows building functions to accomplish a task collaboratively. It allows numerous interdependent components to share states and handle logic while functioning together.
This pattern provides an expressive and versatile API for communication between a parent component and its children. Furthermore, it allows a parent component to communicate implicitly and share state with its children. The Compound components pattern can be implemented using either the context API or the React.cloneElement API.
Refer to the following code example to implement the Compound components pattern with the context API.
import React, { useState, useContext } from "react"; const SelectContext = React.createContext(); const Select = ({ children }) => { const [activeOption, setActiveOption] = useState(null); return ( <SelectContext.Provider value={{ activeOption, setActiveOption }}> {children} </SelectContext.Provider> ); }; const Option = ({ value, children }) => { const context = useContext(SelectContext); if (!context) { throw new Error("Option must be used within a Select component."); } const { activeOption, setActiveOption } = context; return ( <div style={activeOption === value ? { backgroundColor: "black" } : { backgroundColor: "white" }} onClick={() => setActiveOption(value)}> <p>{children}</p> </div> ); }; // Attaching "Option" as a static property of "Select". Select.Option = Option; export default function App() { return ( <Select> <Select.Option value="john">John</Select.Option> <Select.Option value="bella">Bella</Select.Option> </Select> ); }
In the previous example, the select component is a compound component. It comprises various components that share state and behavior. We used Select.Option = Option to link the Option and Select components. Now, importing the Select component automatically incorporates the Option component.
The React Hooks API, introduced in React 16.8, has fundamentally transformed how we approach React component design. Hooks were developed to address common concerns encountered by React developers. They revolutionized the way we write React components by allowing functional components to access features like state, lifecycle methods, context, and refs, which were previously exclusive to class components.
The useState hook enables the addition of state to functional components. It returns an array with two elements: the current state value and a function that allows you to update it.
import React, { useState } from “react”;
import React, { useState } from "react"; function ToggleButton() { const [isToggled, setIsToggled] = useState(false); const toggle = () => { setIsToggled(!isToggled); }; return ( <div> <p>Toggle state: {isToggled ? "ON" : "OFF"}</p> <button onClick={toggle}> {isToggled ? "Turn OFF" : "Turn ON"} </button> </div> ); } export default ToggleButton;
The useEffect hook facilitates performing side effects in functional components. It’s similar to componentDidMount, componentDidUpdate, and componentWillUnmount combined in class components.
import React, { useState, useEffect } from "react"; function Example() { const [data, setData] = useState(null); useEffect(() => { const fetchData = async () => { try { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const jsonData = await response.json(); setData(jsonData); } catch (error) { console.error("Error fetching data:", error); } }; fetchData(); }, []); return ( <div> {data ? ( <div> <h2>Data fetched successfully!</h2> <ul> {data.map((item, index) => ( <li key={index}>{JSON.stringify(item)}</li> ))} </ul> </div> ) : ( <p>Loading data...</p> )} </div> ); } export default Example;
The useRef returns a mutable ref object whose current property is initialized to the passed argument. This object persists for the full lifetime of the component.
import React, { useRef } from "react"; function InputWithFocusButton() { const inputRef = useRef(null); const focusInput = () => { inputRef.current.focus(); }; return ( <div> <input ref={inputRef} type="text" /> <button onClick={focusInput}>Focus Input</button> </div> ); } export default InputWithFocusButton;
Custom hooks enable the creation of reusable functions to extract component logic.
import React, { useState, useEffect } from "react"; function useCustomHook() { const [someState, setSomeState] = useState(null); useEffect(() => { // Some side effect. }, []); return someState; }
This article has covered 5 widely used React component design patterns. Understanding these patterns unlocks React’s full potential in building robust and scalable apps. I encourage you to implement these patterns in your projects to optimize their performance.
Thank you for reading!
For our existing Syncfusion users, the latest version of Essential Studio® is available for download on the License and Downloads page. If you’re new to Syncfusion, we invite you to explore our products’ features and capabilities with our 30-day free trial.
For questions, you can contact us through our support forum, support portal, or feedback portal. We are always here to assist you!