Design Patterns in React

Introduction

I am a software engineer. I love coding, and I know how to code—I’m actually quite good at it. I know how to code and I use most design patterns automatically, but I honestly do not know what they are called (with a few exceptions).

Here are some common design patterns explained with React code:

Singleton Pattern

Ensures a class has only one instance.

Example in React: A global state management instance (e.g., Redux store, Context API provider).

const StoreSingleton = (function () {
  let instance;
  function createInstance() {
    return { user: null, theme: "dark" };
  }
  return {
    getInstance: function () {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();

const store = StoreSingleton.getInstance();

Factory Pattern

Creates objects without specifying the class.

Example in React: Dynamic component rendering based on props.

const ButtonFactory = ({ type, label }) => {
  const components = {
    primary: (label) => <button className="btn btn-primary">{label}</button>,
    secondary: (label) => <button className="btn btn-secondary">{label}</button>,
  };
  return components[type] ? components[type](label) : <button>{label}</button>;
};

// Usage
<ButtonFactory type="primary" label="Click Me" />;

Observer Pattern

Notifies dependent objects when one changes.

Example in React: Event listeners, pub-sub mechanism, React Context API.

import { useState, useEffect } from "react";

const eventListeners = new Set();

const subscribe = (listener) => eventListeners.add(listener);
const unsubscribe = (listener) => eventListeners.delete(listener);
const notifyAll = (data) => eventListeners.forEach((listener) => listener(data));

const useNotification = () => {
  const [message, setMessage] = useState(null);
  useEffect(() => {
    const listener = (data) => setMessage(data);
    subscribe(listener);
    return () => unsubscribe(listener);
  }, []);
  return message;
};

// Usage
const NotificationDisplay = () => {
  const message = useNotification();
  return message ? <p>{message}</p> : null;
};

Decorator Pattern

Dynamically adds features without modifying core code.

Example in React: Higher-Order Components (HOCs) & Custom Hooks.

const withLogging = (WrappedComponent) => {
  return (props) => {
    console.log("Component Rendered:", WrappedComponent.name);
    return <WrappedComponent {...props} />;
  };
};

const Button = (props) => <button {...props}>{props.label}</button>;

const LoggedButton = withLogging(Button);

// Usage
<LoggedButton label="Click Me" />;

Adapter Pattern

Converts incompatible interfaces for seamless interaction.

Example in React: Converting an API response to match UI needs.

const legacyAPIResponse = { fname: "John", lname: "Doe" };

const adaptUser = (data) => ({
  firstName: data.fname,
  lastName: data.lname,
});

const UserProfile = ({ user }) => {
  const adaptedUser = adaptUser(user);
  return <p>{adaptedUser.firstName} {adaptedUser.lastName}</p>;
};

// Usage
<UserProfile user={legacyAPIResponse} />;

Strategy Pattern

Encapsulates algorithms for flexible swapping.

Example in React: Using different sorting algorithms in a table component.

const strategies = {
  ascending: (arr) => [...arr].sort(),
  descending: (arr) => [...arr].sort().reverse(),
};

const SortableList = ({ items, strategy }) => {
  const sortedItems = strategies[strategy](items);
  return <ul>{sortedItems.map((item) => <li key={item}>{item}</li>)}</ul>;
};

// Usage
<SortableList items={["Banana", "Apple", "Cherry"]} strategy="ascending" />;

Command Pattern

Wraps requests as objects to allow undo/redo.

Example in React: Undo/redo for a text editor.

import { useState } from "react";

const useCommand = () => {
  const [history, setHistory] = useState([]);
  const [index, setIndex] = useState(-1);

  const execute = (command) => {
    const newHistory = history.slice(0, index + 1);
    newHistory.push(command);
    setHistory(newHistory);
    setIndex(newHistory.length - 1);
    return command.execute();
  };

  const undo = () => {
    if (index >= 0) {
      const command = history[index];
      setIndex(index - 1);
      return command.undo();
    }
  };

  return { execute, undo };
};

Composite Pattern

Treats objects and compositions the same way.

Example in React: Recursive component rendering.

const Comment = ({ text, replies }) => (
  <div>
    <p>{text}</p>
    {replies && replies.map((reply, i) => <Comment key={i} {...reply} />)}
  </div>
);

Conclusion

I use most of these design patterns daily—but I don’t know what they are called. I know I don’t like a services pattern (oh sorry, the Facade pattern) as I like keeping all my fetching code together in my Context. Though, to be honest, I am starting to wonder why.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.