React Design Patterns, Part 1 Compound Components.

React Design Patterns, Part 1 Compound Components.

When i started my journey as a Junior React Developer, i started to build things from scratch and all goes well for me at first. I needed sometimes to reiterate over some components to add more requirements and i was able to refactor them for the first iterations, but after a while it started to be difficult to add functionalities to existing components or to explain the existing code base to a newcomer to the team. Fortunately a senior newcomer had some good solutions for us that we didn’t understand nor accept at first, but with some effort and researches all became clear and pleasant again. These new ways of writing code without repeating ourself and that gives more control to the consumer of the components rather than be usable for one use case. These amusing ways of writing components are called Design Patterns, and we will visit a lot of them in this serie, so prepare yourself to be a rockstar of the react world.

In this serie we are going to learn about different component patterns by building a simple accordion component.

First we will explore a naive implementation, to understand the problem space, here is a link to the starter code.

// index.js

import React from 'react'
import { render } from 'react-dom'

import Accordion from './Accordion'
import { data } from './data'

import 'normalize.css'
import './styles.css'

const App = () => {
	return (
		<div className="App">
			<Accordion
				items={data} // Required, array of data objects
			/>
		</div>
	)
}

const rootElement = document.getElementById('root')
render(<App />, rootElement)
// Accordion.js

import React, { useState } from 'react'
import AccordionItem from './AccordionItem'
import ActiveItem from './ActiveItem'

const Accordion = ({ items }) => {
	const [activePanel, setActivePanel] = useState(-1)

	const handleClick = id => {
		const value = id === activePanel ? -1 : id
		setActivePanel(value)
	}

	return (
		<>
			<div className="panels">
				{items.map(item => (
					<AccordionItem
						key={item.itemId}
						item={item}
						onClick={() => handleClick(item.itemId)}
						activePanel={activePanel}
					>
						<p style={{ fontSize: '15pt', color: 'white' }}>{item.title}</p>
					</AccordionItem>
				))}
			</div>
			<ActiveItem activePanel={activePanel} items={items} />
		</>
	)
}

export default Accordion

If we assume that we publish the Accordion component to npm and the user/consumer of this component will only import it like this: import Accordion from "accordion";, then it’s flexibility ends at the items prop; we are only able to change the items to display. What if we need to position the AccordionItem bellow ActiveItem?

Let’s try a different approach and rewire the component so it has flexibility and reusability to be used in any future configurations.

Compound Components

Compound components are groupings of smaller components that all work together and are exposed as subcomponents of a single root. Those can be exposed via exports from a folder's index file, or as properties on a main component.

Example usage exposed from an index file:

import {
	ParentComponent,
	ChildComponent1,
	ChildComponent2,
} from 'components/page'

;<ParentComponent>
	<ChildComponent1>Some content 1.</ChildComponent1>
	<ChildComponent2>Some content 2.</ChildComponent2>
</ParentComponent>

Example usage exposed as properties of a single component:

import { ParentComponent } from 'components/page'

;<ParentComponent>
	<ParentComponent.ChildComponent1>
		Some content.
	</ParentComponent.ChildComponent1>
	<ParentComponent.ChildComponent2>
		Some content.
	</ParentComponent.ChildComponent2>
</ParentComponent>

Our demos are built using the compound component described in the second example above.

This second example can be acheived by making the child components as static class properties on the larger parent class component

class ParentComponent extends React.Component {
	static ChildComponent1 = () => {
		// code here
	}

	static ChildComponent2 = () => {
		// code here
	}

	// code here
}

Or if you like the functional way like this

const ParentComponent = () => {
	// code here
}

// This is equivalent to static properties for classes

ParentComponent.ChildComponent1 = () => {
	// code here
}

ParentComponent.ChildComponent2 = () => {
	// code here
}

Using the compound component pattern, we can let the consumer decide how many or what order they want to render their sub-components. This idea is called inversion of control and it gives more power back to the consumer of your component and this makes your component more flexible and more scalable.

Let's go back to our Accordion component as you've noticed the only thing that we exposed as API is the items prop

<Accordion
	items={data} // Required, array of data objects
/>

This is not a reusable nor a flexible API

Let's use the magic of compound components and see, but before that we need to understand two small utilities from the React library React.Children.map and React.cloneElement.

React.Children.map is similar to Array.map as it's used to map over this.props.children and return an array, but as you know this.props.children can be an array if we have more than one child but can also be a single node element if we have one child, so React.Children.map convert the single node element to an array before mapping over it.

Remember this example

import { ParentComponent } from 'components/page'

;<ParentComponent>
	<ParentComponent.ChildComponent1>
		Some content.
	</ParentComponent.ChildComponent1>
	<ParentComponent.ChildComponent2>
		Some content.
	</ParentComponent.ChildComponent2>
</ParentComponent>

We use React.Children.map like in the render method here:

class ParentComponent extends React.Component {
	static ChildComponent1 = () => {
		// code here
	}

	static ChildComponent2 = () => {
		// code here
	}

	render() {
		return React.Children.map(this.props.children, child => {
			// code here
		})
	}
}

React.cloneElement(element, additionalProps) Clone and return a new React element using element as the starting point. The resulting element will have the original element’s props with the new additionalProps merged in.

import OriginalComponent from "OriginalComponent";

const NewComponent = React.cloneElement(OriginalComponent, {{ foo: "bar" }})

NewComponent is the clone (same definition) of OriginalComponent except that it has in addition the prop foo.

Using these techniques I am able to design components that are completely reusable, and have the flexibility to use them in a number of different contexts.

Here is the Accordion component with the Compound components pattern:

  • Usage
// index.js

import React from 'react'
import { render } from 'react-dom'

import Accordion from './Accordion'
import { data as items } from './data'

import 'normalize.css'
import './styles.css'

const App = () => {
	return (
		<div className="App">
			<div className="panels">
				<Accordion>
					{items.map(item => {
						return (
							<Accordion.AccordionItem key={item.itemId} item={item}>
								{item.title}
							</Accordion.AccordionItem>
						)
					})}
				</Accordion>
				<div />
			</div>
		</div>
	)
}

const rootElement = document.getElementById('root')
render(<App />, rootElement)
  • Definition
// Accordion.js

import React, { Component } from 'react'

class Accordion extends Component {
	state = {
		activePanel: -1,
	}
	static AccordionItem = ({ children, item, activePanel, onClick }) => {
		return (
			<div
				className={`panel${
					activePanel === item.itemId ? ' open open-active' : ''
				}`}
				style={{
					backgroundImage: `url("${item.imageUrl}")`,
					height: '500px',
				}}
				onClick={() => onClick(item.itemId)}
			>
				<p>{children}</p>
			</div>
		)
	}

	handleClick = id => {
		const value = id === this.state.activePanel ? -1 : id
		this.setState({ activePanel: value })
	}

	render() {
		const { activePanel } = this.state
		return React.Children.map(this.props.children, child => {
			return React.cloneElement(child, {
				activePanel,
				onClick: this.handleClick,
			})
		})
	}
}

export default Accordion

In line 9, we declared AccordionItem as a static proprety of the Accordion class component, so it's namespaced, and we can access it like this Accordion.AccordionItem by only importing the class Accordion.

In line 33 to 38, we mapped over the childrens of Accordion and add to them the activePanel state as prop by cloning them, so from now on, the activePanel state is implicit and we can't acceess it when using this API:

<Accordion>
	<Accordion.AccordionItem item={item}>{item.title}</Accordion.AccordionItem>
</Accordion>

Here is a link to the Accordion compond component that implements the above functionalities.

More flexible Compound Components with the help of context API

Suppose for stylistic purpose we want to wrap <Accordion.AccordionItem /> component in a div like this:

<Accordion>
	<div className="panels">
		{items.map(item => {
			return (
				<Accordion.AccordionItem key={item.itemId} item={item}>
					{item.title}
				</Accordion.AccordionItem>
			)
		})}
	</div>
	<Accordion.ActiveItem items={items}>{item.title}</Accordion.ActiveItem>
</Accordion>

This will break our Accordion and the reason is that when we mapped over and cloned children :

return React.Children.map(this.props.children, child => {
	return React.cloneElement(child, {
		activePanel,
		onClick: this.handleClick,
	})
})

When cloning, we gave the additional props to the direct child, which is the div element in the example above not the <Accordion.AccordionItem /> component.

To solve this problem we can use the context API and pass this implicit state to the childs whatever their depth in the hirarchy:

// Accordion.js

import React, { Component } from 'react'

const AccordionContext = React.createContext({
	activePanel: -1,
	onClick: () => {},
})

class Accordion extends Component {
	static AccordionItem = ({ children, item }) => {
		return (
			<AccordionContext.Consumer>
				{({ activePanel, onClick }) => (
					<div
						className={`panel${
							activePanel === item.itemId ? ' open open-active' : ''
						}`}
						style={{
							backgroundImage: `url("${item.imageUrl}")`,
							height: '500px',
						}}
						onClick={() => onClick(item.itemId)}
					>
						<p style={{ fontSize: '15pt', color: 'white' }}>{children}</p>
					</div>
				)}
			</AccordionContext.Consumer>
		)
	}

	static ActiveItem = ({ items }) => {
		return (
			<AccordionContext.Consumer>
				{({ activePanel }) =>
					activePanel !== -1 ? (
						<h2>{items.find(({ itemId }) => activePanel === itemId).title}</h2>
					) : (
						<h2>Select a photo</h2>
					)
				}
			</AccordionContext.Consumer>
		)
	}

	handleClick = id => {
		const value = id === this.state.activePanel ? -1 : id
		this.setState({ activePanel: value })
	}

	state = {
		activePanel: -1,
		onClick: this.handleClick,
	}

	render() {
		return (
			<AccordionContext.Provider value={this.state}>
				{this.props.children}
			</AccordionContext.Provider>
		)
	}
}

export default Accordion

From line 5 to 8, we create the AccordionContext, and from line 58 to 60, we gave the AccordionContext.Provider the state as a value, we made the onClick handler in the state for optimisation purpose, and we used this value in the static component with the help of the AccordionContext.Consumer. This way we are able to pass this implicit state in any deeply nested component.

Here is a link to the Accordion compond component with the context API.

In the next part of this serie, we will discuss how we can use render props to achieve the same goals without having to rely on wiring up context to share state between components in our application.

Bouamar El Ouahhabi
Bouamar El Ouahhabi
2019-12-17 | 9 min read
Share article

More articles