React Compound Components

The compound component pattern is made of two or more components sharing an implicit state and logic to produce a complete UI.

We can think of compound components like the <select> and <option> elements in HTML. The two elements together produce an HTML menu, but <select> or <option> alone are useless.

<select name="pets"> <option value="dog">Dog</option> <option value="cat">Cat</option> </select>

The compound component pattern allows us to create declarative components. But more importantly, it gives more rendering control. This is particularly useful for handling variations without having to explicitly define behavior via props.

Compound components functionality stays intact while providing flexibility to handle many use-cases.

Let's explore the compound component pattern by building a reusable accordion component.

Edit react-accordion


Composition

When creating reusable components we want to provide flexibility. In the context of our accordion, we can think of many use-cases. For example: support layout variations inside the collapse panels.

If we try to handle these layout variations via props, the accordion component will have to handle this logic internally. This is not the best approach because it will over-complicate the internal logic trying to solve every conceivable scenario.

A better approach is to use React.children and handle these variations externally.

Let's look at the <Accordion> component structure.

Each <Accordion.item> represents a single panel and inside each item we have <Accordion.Toggle> and <Accordion.Collapse>.

<Accordion onChange={id => setActive(id)} expanded={active}> <Accordion.Item> <Accordion.Toggle id="01">One</Accordion.Toggle> <Accordion.Collapse> ANY CONTENT TYPE (HTML, img, iframe, etc..) </Accordion.Collapse> </Accordion.Item> <Accordion.Item> <Accordion.Toggle id="02">Two</Accordion.Toggle> <Accordion.Collapse>CUSTOM LAYOUT</Accordion.Collapse> </Accordion.Item> </Accordion>

<Accordion> has two props: onChange and expanded. These two props need to be shared with every <Accordion.Item>. This is how each item will know the common state or trigger a state change.

React.Children.map

To iterate over the <Accordion> children and pass down these props, we can use React.Children.map.

React.Children.map(children, function[(thisArg)])

React.Children.map second function gives us access to each <Accordion.Item> component. Now we can pass the required props to each <Accordion.Item>. But first we need to clone the element.

React.cloneElement

To add the required props to each child we must clone and return a new React element. The resulting element will have the original element’s props with the new props merged in shallowly.

If we don't clone the element, React will throw a TypeError: Cannot add property on, object is not extensible.

const Accordion = ({ children, expanded = '', onChange }) => { return ( <Container> {React.Children.map(children, ({ props: { children } }) => { const toggleElem = children[0]; const collapseElem = children[1]; const isExpanded = expanded === toggleElem.props.id; const toggleElemCloned = React.cloneElement(toggleElem, { toggle: (id: string) => onChange(id), isExpanded, }); return ( <AccordionItem> {toggleElemCloned} {isExpanded && collapseElem} </AccordionItem> ); })} </Container> ); };

Each <Accordion.Item> will have a:

  • const toggleElem = children[0] // is <Accordion.Toggle>
  • const collapseElem = children[1] // is <Accordion.Collapse>

We cloned the <Accordion.Toggle> and added the required props: toggle and isExpanded. Finally, we can use isExpanded to show or hide the correct <Accordion.Collapse> panel.

AccordionToggle.tsx <Accordion.Toggle>

const AccordionToggle = ({ id, children, toggle, isExpanded = false }) => { const handleClick = () => toggle(isExpanded ? '' : id); return ( <div onClick={handleClick} expanded={isExpanded}> {children} </div> ); };

AccordionCollapse.tsx <Accordion.Collapse>

const AccordionCollapse = ({ children }) => <div>{children}</div>;

Summary

It's not recommended to try to solve every conceivable scenario inside our component. This will over complicate our code. The compound component pattern is a great approach to offer flexibility in our reusable components while keeping the internal logic of our components intact.

The final version of the accordion component is using styled components for the CSS, and Framer motion for the CSS animation.

Edit react-accordion