Understanding Compound Components in React - Bits and Pieces

December 30, 2019 0 Comments

Understanding Compound Components in React - Bits and Pieces

 

 

Aayush Jaiswal
Dec 30, 2019 · 7 min read

So, you have created a component and you are proud of it, but then the designs have changed a bit, and you are forced to pass a prop like leftAlignment={false} or isBottom to adjust your component. Okay, this works but it also shows that there are some flaws with this pattern. And this is what this post is for, to make you understand and use compound components so you can avoid this side effect of passing extra props to handle minor design changes, so without any further ado, let’s begin.

Compound component pattern may seem a bit tricky at first but don’t worry, by the end of this article you will clearly understand them.

Tip: Building React components as reusable pieces of code is a great start but certainly not enough. Use tools like Bit (Github) to share your components to a component collection in bit.dev. It would make your components reusable across repositories and easily discoverable, for you and your team.

Example: searching for shared components it bit.dev

What are compound components?

Think of compound components like the <select> and <option> elements in HTML. Apart they don’t do too much, but together they allow you to create the complete experience. — Kent C. Dodds

Like <select> and <option> work. You can click on option and select knows which one was clicked. They work together and so do compound components.

Ever used semantic-ui-react or ant-design or any other UI based react library? If you have, you must have noticed something. The way their components work. Let’s take a look at semantic-ui-react Dropdown component.

import React from ‘react’
import { Dropdown } from ‘semantic-ui-react’
const DropdownExampleDropdown = () => (
<Dropdown text=’File’>
<Dropdown.Menu>
<Dropdown.Item text=’New’ />
<Dropdown.Item text=’Open…’ description=’ctrl + o’ />
<Dropdown.Item text=’Save as…’ description=’ctrl + s’ />
<Dropdown.Item text=’Rename’ description=’ctrl + r’ />
<Dropdown.Item text=’Make a copy’ />
<Dropdown.Item icon=’folder’ text=’Move to folder’ />
<Dropdown.Item icon=’trash’ text=’Move to trash’ />
<Dropdown.Divider />
<Dropdown.Item text=’Download As…’ />
<Dropdown.Item text=’Publish To Web’ />
<Dropdown.Item text=’E-mail Collaborators’ />
</Dropdown.Menu>
</Dropdown>
)
export default DropdownExampleDropdown

See, the Dropdown component works together with Dropdown.Menu , Dropdown.Divider and Dropdown.Item as a team. Now, the user of Dropdown component can decide where he wants to place the divider and we don’t have to pass in an extra prop to do that.

So how do we create these compound components 🤔 ?

  • React.cloneElement
  • Using Context API (we’ll use hooks)

React.cloneElement

import DropdownDivider from './DropdownDivider'
import DropdownItem from './DropdownItem'
import DropdownHeader from './DropdownHeader'
import DropdownMenu from './DropdownMenu'
export default class Dropdown extends Component{ static propTypes = {} static Divider = DropdownDivider
static Header = DropdownHeader
static Item = DropdownItem
static Menu = DropdownMenu
// functions and lifecycle hooks to handle the logic renderMenu = () => {
// logic
// uses React.cloneElement()
}
render() {
// returns data to be rendered
return this.renderMenu();
}
}

The above is a very tiny bit of the original code, but I want to get your attention on two things:

1. static Divider = DropdownDivider  
static Header = DropdownHeader
static Item = DropdownItem
static Menu = DropdownMenu
2. React.cloneElement()

This first point allows us to access the DropdownDivider or DropdownHeader components in our files like this Dropdown.Divider and Dropdown.Header .

So we can use these components with importing only Dropdown component in our files.

The second point is React.cloneElement. What does it do?

React.cloneElement(
element,
[props],
[...children]
);

The name says it all. It creates a copy of an element and merges new props into it. And this is what React docs say:

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 props merged in shallowly. New children will replace existing children. key and ref from the original element will be preserved.

Also, we need another API to work out with cloneElement, React.Children.map

As per react docs,

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

Invokes a function on every immediate child contained within children with this set to thisArg. If children is an array it will be traversed and the function will be called for each child in the array. If children is null or undefined, this method will return null or undefined rather than an array.

In layman terms, you can think of it as map for now. Enough talk, let’s write some code ✌️

We would like to create a Tabs component which would work with other components like the TabPanels, TabPanel and Tab component, and would look altogether something like this

<Tabs>   <Tab id="a">Coco</Tab>   <Tab id="b">Up</Tab>   <TabPanels>      <TabPanel id="a">Miguel</TabPanel>      <TabPanel id="b">Russell</TabPanel>   </TabPanels></Tabs>

Now, look at the benefits of this, the design changes and the tabs are to be below the Panels, anybody using the component can do it by himself and doesn’t have to pass any prop like isPanelsBottom or anything.

<Tabs>

<TabPanels>
<TabPanel id="a">Miguel</TabPanel> <TabPanel id="b">Russell</TabPanel> </TabPanels> <Tab id="a">Coco</Tab> <Tab id="b">Up</Tab></Tabs>

So how would we write this component, let’s make our plan:

We need 4 components Tabs TabPanels TabPanel and Tab . We will keep our state and functions in the parent component so that all the child components can have access to those. Tabs being the parent component, let’s write it first:

Tabs.js

So, we have the activeId state that keeps track of current id and handleClick that changes the activeId. We pass these two as props to the child components using React.Children and React.cloneElement explained above.

And our child component’s would look like this:

Tab.js

The Tab component has a button on clicking it, it calls for the handleClick function with the current id.

TabPanels.js

TabPanels being the direct children component have access to Tabs component props, but note, the children of TabPanels don’t, hence we have to pass the props to TabPanels children using clone and map approach again (biggest drawback for this method).

And finally, our TabPanel which will render data when it’s id matches to the activeId

TabPanel.js

But we didn’t do anything like this, <Tabs.Tab> or Tabs.Panels 😒. Yes we can do this too, by a slight change in our Tabs component.

Tabs.js

You can check the code here,

We saw one major drawback to this approach is that to have access to the parent component’s prop one should be a direct child for the parent. So if we add a div between our compound component it will most likely break. And to solve this we use the second approach, context API.

Context API

I will be using hooks this time to explain this approach.

Same logic in the parent component (Tabs component), we will have our activeId state and handleClick method. Instead of using React.Children and React.cloneElement we will pass our state using context. Our context will look something like this:

export const tabContext = React.createContext({ activeId: "", handleClick: () => {}});

And our Tabs component will provide our state and method using context:

Tabs.js

And we will get the context using useContext hook, in our child components:

TabPanel.js

The logic is the same as before for both TabPanel and Tab component, instead of getting data as props we get it from context.

Tab.js

The context approach seems more easy and clean to follow and also avoids the side-effects of the former approach.

Our compound component can be used as follows:

<Tabs>  <div>    <Tabs.Tab id="a">Coco</Tabs.Tab>    <Tabs.Tab id="b">Up</Tabs.Tab>  </div>  <div>    <Tabs.TabPanel id="a">Miguel</Tabs.TabPanel>    <Tabs.TabPanel id="b">Russell</Tabs.TabPanel>  </div></Tabs>

Here is the sandbox for the above code,

Conclusion

I hope you liked this article and learned something new and if you did clap your heart out and follow me for more content on Medium and as well as on Twitter. Please feel free to comment and ask anything. Thanks for reading 🙏 💖.

Learn More


Tag cloud