React Design Patterns: A Practical Guide

React Design Patterns: A Practical Guide

·

10 min read

TL;DR: The 5 essential React design patterns are the HOC, 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.

1. The HOC (Higher Order Component) pattern

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.

Pros

  • Helps maintain reusable functionality in a single place.

  • Keeps the code clean and implements separation of concerns by consolidating all logic into a single piece.

  • Reduces the possibility of unexpected bugs across the app by avoiding code duplication.

Cons

  • Sometimes leads to props name conflicts, making debugging and scaling an app more challenging, especially when composing a component with many HOCs that share identical prop names.

2. Provider pattern

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.

Implementing Provider pattern with React-Redux

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

Provider pattern with React Context

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

Consuming Context data

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

Pros

  • Allows sending data to several components without going through the component hierarchy.

  • Reduces the possibility of unforeseeable errors when refactoring code.

  • Eliminates prop-drilling, an anti-pattern.

  • Simplifies preserving some form of global state since components can access it.

Cons

  • Overusing the Provider pattern might cause performance concerns, especially when passing a constantly changing variable to multiple components. However, this won’t be a significant problem in smaller apps.

3. Container/Presentational pattern

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.

Presentational components

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.

Container components

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

Pros

  • Separation of concerns is implemented.

  • Presentational components are highly reusable.

  • Since presentational components do not alter app logic, their appearance can be modified without understanding the source code.

  • Testing presentational components is straightforward, as these components render based on the provided data.

Cons

  • With the Container/Presentational pattern, stateless functional components need to be rewritten as class components.

4. Compound pattern

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.

Pros

  • Compound components maintain their internal state, which is shared among child components. Thus, there’s no need to manage the state explicitly while using a compound component.

  • Manual import of child components is unnecessary.

Cons

  • Component stacking is limited when using React.Children.map to pass values. Only immediate children of the parent component will have access to the props.

  • Naming clashes may occur if an existing prop has the same name as the props provided to React.cloneElement method when cloning an element with React.cloneElement.

5. Hooks pattern

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.

useState

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;

useEffect

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;

useRef

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

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

Pros

  • Organizes code and makes it neat and clear, unlike lifecycle methods.

  • Overcomes the challenge of maintenance, utilizing hot reloading, and minification issues.

  • Allows leveraging state and other React capabilities without writing a class.

  • Facilitates the reuse of stateful logic across components, reducing code duplication.

  • Reduces the possibility of mistakes and enables composition with simple functions.

Cons

  • Requires adherence to specific rules, although it’s challenging to identify rule violations without a linter plugin.

  • Demands practice to utilize certain hooks effectively (e.g., useEffect).

  • Necessitates caution to avoid inappropriate usage (e.g., useCallback, useMemo).

Conclusion

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!