DEV Community

Cover image for Compound Components Pattern in React
Bidisha Das
Bidisha Das

Posted on

Compound Components Pattern in React

During development, we face some design patterns in React. Compound Components is one of the most important and frequently used Design Pattern in React. Let us create a Expandable Accordion component using React.

Compound Components are components that are made up of two or more components which cannot be used without it's parent.

A select box is an example of it.

Image description

Intially, we set up the Expandable component. Here is the code that goes along with it.

import React, {createContext} from React;
const ExpandableContext = createContext();
const {Provider} = ExpandableContext;

const Expandable = ({children}) => {
    return <Provider>{children}</Provider>
}

export default Expandable;
Enter fullscreen mode Exit fullscreen mode

The following things are happening here

  1. ExpdandableContext is created,
  2. The Provider is desctructured from the ExpandableContext
  3. In the end, we are just creating an Expandable Component and returning the JSX with the Provider that displays the children passed to the Expandable component

Now we have to introduce state for the expanded accordion and even create a toggle function for it.

const Expandable = ({children}) => {

    /**
     * State to update the expanded behaviour
     */
    const [expanded, setExpanded] = useState(false);

    /**
     * Method for toggling the expanded state
     */
    const toggle = setExpanded(prevExpanded => !prevExpanded);

    return <Provider>{children}</Provider>
}
Enter fullscreen mode Exit fullscreen mode

Now the toggle callback function will be invoked by the expandable header and it shouldn't change every time or re-render. Hence, we can memoize the callback as follows.

After this, we need to pass these - toggle function and expanded to the provider. Hence we write this line:

const value = { expanded, toggle }
Enter fullscreen mode Exit fullscreen mode

and to prevent the re-rendering of value every time, we use useMemo for preserving the object on every render.

const value = useMemo(()=> {expanded, toggle}, [expnded, toggle]);
Enter fullscreen mode Exit fullscreen mode

Providing flexibility to the external user to provide custom functionality after expansion

At times, it will be the requirement to provide custom functionality to the user after the accordion is expanded. In this case we can follow the below pattern.

For class components we can do this using a callback, however for functional components we need to do this with useeffect and run this only when the functional component has already been mounted (it should not run when the component is mounted every time).

     * Check for mounting
     */
    const componentJustMounted = useRef(true);

    /**
     * Function to call when the expanded state is altered tp true, 
     * that is when the expansion happens. 
     */
    useEffect(()=> {
        if(!componentJustMounted.current){
            onExpand(expanded);
        }
        componentJustMounted.current = false
    }, [expanded]) 
Enter fullscreen mode Exit fullscreen mode

We are using a useRef as it will return a reference which will be preserved during render cycles. Initially it is set to true. We only make it false when the callback is executed with the expanded prop passed to it.

Hence the whole component Expandable.js looks like this:

import React, {createContext, useState, useCallback, useRef, useEffect} from 'react';
const ExpandableContext = createContext();
const {Provider} = ExpandableContext;

const Expandable = ({children}) => {

    /**
     * State to update the expanded behaviour
     */
    const [expanded, setExpanded] = useState(false);

    /**
     * Check for mounting
     */
    const componentJustMounted = useRef(true);

    /**
     * Function to call when the expanded state is altered tp true, 
     * that is when the expansion happens. 
     */
    useEffect(()=> {

        if(!componentJustMounted.current){
            onExpand(expanded);
        }
        componentJustMounted.current = false
    }, [expanded, onExpand])

    /**
     * Method for toggling the expanded state
     */
    const toggle = useCallback(() => 
        setExpanded(prevExpanded => !prevExpanded), []
    );

    const value = useMemo(()=> {expanded, toggle}, [expanded, toggle])

    return <Provider value={value}>{children}</Provider>
}

export default Expandable;

Enter fullscreen mode Exit fullscreen mode

Building Child Components

The three components of the body, header and icon are as follows.

Header.js

import React, { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Header = ({children}) => {
  const { toggle } = useContext(ExpandableContext)
  return <div onClick={toggle}>{children}</div>
}
export default Header; 
Enter fullscreen mode Exit fullscreen mode

Here we just try and access the toggle and on click we toggle the body on click of the div. This is the by default feature of accordion.

For Body,

Body.js

import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children }) => {
  const { expanded } = useContext(ExpandableContext)
  return expanded ? children : null
}
export default Body
Enter fullscreen mode Exit fullscreen mode

In the body, we check if the expanded property is true or not. If it is true, we set the body to the props.children passes to it, otherwise we return null (since the body is not expanded).

For icon, we can use Icon.js which looks like this:

Icon.js

// Icon.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Icon = () => {
  const { expanded } = useContext(ExpandableContext)
  return expanded ? '-' : '+'
}
export default Icon
Enter fullscreen mode Exit fullscreen mode

For expanded body, we show a - sign and for contracted body, we show, +.

After adding these logics, let us add just the styles in the each of these elements and finally the components look like this.

Expandable.js

import React, {
  createContext,
  useState,
  useCallback,
  useRef,
  useEffect,
  useMemo,
} from "react";
export const ExpandableContext = createContext();
const { Provider } = ExpandableContext;

const Expandable = ({ onExpand, children, className = "", ...otherProps }) => {
  const combinedClasses = ["Expandable", className].filter(Boolean).join("");

  /**
   * State to update the expanded behaviour
   */
  const [expanded, setExpanded] = useState(false);

  /**
   * Check for mounting
   */
  const componentJustMounted = useRef(true);

  /**
   * Method for toggling the expanded state
   */
  const toggle = useCallback(
    () => setExpanded((prevExpanded) => !prevExpanded),
    []
  );

  /**
   * Function to call when the expanded state is altered tp true,
   * that is when the expansion happens.
   */
  useEffect(() => {
    if (!componentJustMounted.current) {
      onExpand(expanded);
    }
    componentJustMounted.current = false;
  }, [expanded, onExpand]);

  const value = useMemo(() => ({ expanded, toggle }), [expanded, toggle]);

  return (
    <Provider value={value}>
      <div className={combinedClasses} {...otherProps}>{children}</div>
    </Provider>
  );
};
export default Expandable;
Enter fullscreen mode Exit fullscreen mode

Body.js

// Body.js
import './Body.css'
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children , className='',... otherProps}) => {
  const { expanded } = useContext(ExpandableContext);
  const combinedClassName = ['Expandable-panel', className].filter(Boolean).join('');
  return expanded ? 
  <div className ={combinedClassName} {...otherProps} >{children}</div> : null
}
export default Body
Enter fullscreen mode Exit fullscreen mode

Header.js

import React, { useContext } from 'react'
import { ExpandableContext } from './Expandable'
import './Header.css';
const Header = ({className='', children, ...otherProps}) => {

  const combinedClassName = ['Expandable-trigger',className].filter(Boolean).join('');

  const { toggle } = useContext(ExpandableContext)
  return <button className={combinedClassName} {...otherProps}
  onClick={toggle}>{children}</button>
}
export default Header;
Enter fullscreen mode Exit fullscreen mode

Icon.js

import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Icon = ({ className='', ...otherProps}) => {
  const { expanded } = useContext(ExpandableContext);
  const combinedClassName = ['Expandable-icon', className].join('');
  return <span className={combinedClassName} {...otherProps}>{expanded ? '-' : '+'}</span>
}
export default Icon
Enter fullscreen mode Exit fullscreen mode

You can view its behaviour at https://officialbidisha.github.io/exapandable-app/

and the github code is available at https://github.com/officialbidisha/exapandable-app

This is how compound components work. We cannot use the Expandable component without the Header, Icon and Body and vice versa. We have successfully learnt a design pattern now.

Happy learning!

Top comments (9)

Collapse
 
amankumarsingh01 profile image
A

If you would like to add syntax coloring to your code:

Markdown - how to make blocks of React code (syntax highlighting - github, dev.to)

I personally struggeled with this.

At the beginning of the src code line after 3 x the grave accent sign, we need to add jsx.

Collapse
 
pcjmfranken profile image
Peter Franken • Edited

Like so:

# Markdown

Space, the final frontier. These are the voyages of the Starship Enterprise. Its five-year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before.

Code block:

`​``jsx
import React from 'react'

export default function Hai({ children }){
  return '<h1>{children}</h1>'
}
``​`

Fin.
Enter fullscreen mode Exit fullscreen mode
Collapse
 
tsgoswami profile image
Trishnangshu Goswami

Thanks for putting the source code link. Its helped me understand the whole picture in a single shot. Nice article. A new thing learned. Keep writing ✍️. Keep sharing your knowledge. Wish you all the best.

Collapse
 
officialbidisha profile image
Bidisha Das

Thank you so much. Again, you are the inspiration.

Collapse
 
officialbidisha profile image
Bidisha Das

Thank you so much for this. Much needed. Going through this pattern. Thank you for your contributions.

Collapse
 
officialbidisha profile image
Bidisha Das • Edited

Also wanted to ask, why did you convert the inline styles.
I applied the inline style to demonstrate the purpose of ...otherProps.

 
officialbidisha profile image
Bidisha Das

Thank you 🙏

Collapse
 
tsgoswami profile image
Trishnangshu Goswami

Is 2 the minimum number of components required to build compound components?

Collapse
 
officialbidisha profile image
Bidisha Das

Yes right. You are right.