React Patterns in Action

Compound Pattern

This pattern is based on the idea of composing components together in a way that they can share state and logic, while still maintaining a clear and manageable structure.

This is achieved by having a parent component (the compound component) that encapsulates and manages the shared state and logic, and child components that consume and interact with this state.

Instead of creating a hierarchy of components through inheritance, the Compound Pattern uses composition to build complex UIs. This makes the components more reusable and easier to reason about.

The relationship between the parent component and its children is explicit and is often achieved through React's context API or render props.

It is particularly useful for building complex, interactive components like forms, menus, tabs, and accordions. It allows for the creation of a cohesive component interface while keeping the individual pieces.

Let's build a reusable Accordion component system to better understand how it is working.

We will start from scratch and will create a base for our component system.

accordion/accordion.js

import { createContext, useContext } from 'react';

const AccordionContext = createContext();

export const Accordion = ({ children }) => {
  return (
    <AccordionContext.Provider>
      <div className="accordion">{children}</div>
    </AccordionContext.Provider>
  );
}

export const useAccordion = () => useContext(AccordionContext);

At the code above we are doing several things:

  • AccordionContext - context will be used to provide and consume data related to the accordion component.
  • AccordionContext.Provider is used to wrap the children. This provider makes the context available to all the children components and any of their descendants.
  • useAccordion - this custom hook simplifies the process of accessing the accordion's context in any child component

Now, let's move forward and update our code a little bit.

import { createContext, useContext, useState } from 'react';

export const Accordion = ({ children }) => {
  const [activeIds, setActiveIds] = useState([]);

  const toggleItem = (id) => {
    setActiveIds((currentActiveIds) => {
      if (currentActiveIds.includes(id)) {
        return currentActiveIds.filter((item) => item !== id);
      } else {
        return [id];
      }
    });
  };

  return (
    <AccordionContext.Provider value={{ activeIds, toggleItem }}>
      <div className="accordion">{children}</div>
    </AccordionContext.Provider>
  );
};

Here, we are keeping track f the IDs of the accordion items that are currently active (expanded).

toggleItem function is responsible for toggling the state of the accordion items. When an item's header is clicked, this function will be invoked with the item's unique identifier id.

AccordionContext.Provider now receives a value prop with activeIds and toggleItem so that we can pass it to the child components.

Now, its time to import Accordion to the App, but first let's make our import syntax cleaner and add this code to the accordion/index.js file.

export * from './accordion';

After this, we can easily import it into the App component.

import { Accordion } from './accordion';

export default function App() {
  return <Accordion>I am accordion!</Accordion>
}
Files
FileDirectory
  • Directoryaccordion[+]
    • Fileaccordion.js
      • Fileindex.js
      • Directorypublic[+]
        • Fileindex.html
        • FileApp.js
          • Fileindex.jsentry
            • Filepackage.json
              • Filestyles.css
                export default function App() {
                  return <h1>Hello world</h1>
                }