React in General is less opinionated about How you structure your code, what type of styling pattern you choose(e.g. CSS,scss, CSS module, CSS in js), and what type of state management library you choose(e.g redux,react-recoil, flux, jotai, zustand...). the good thing about having less opinion is that React community comes up with amazing patterns that are more declarative, maintainable, abstract, and testable over the years. for example render props, HOC, custom hooks. And one of that pattern is the Compound component .
Idea behind the compound component is that two or more components work together to create more abstract, maintainable, and declarative UI components. Well to be more precise Here is a quote from great kent c dodds.
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 a complete experience.
- Before discussing too much about the pattern let's just look at code and then we will discuss how it can make our life easier.
<Modal show={showModal} onClose={hideModalHandler}>
<Modal.Header>stateless Compound Component</Modal.Header>
<p className="contentContainer">
Contrary to popular belief, Lorem Ipsum is not simply random text. It
has roots in a piece of classical Latin literature from 45 BC, making
it over 2000 years old.
</p>
<Modal.Footer>
<div>
<button onClick={hideModalHandler}>Cancel</button>
<button onClick={hideModalHandler}>Confirm</button>
</div>
</Modal.Footer>
</Modal>
<Modal show={showModal} onClose={hideModalHandler} title="without Compound Component" footer={
<div>
<button onClick={hideModalHandler}>Cancel</button>
<button onClick={hideModalHandler}>Confirm</button>
</div>
}>
<p className="contentContainer">
Contrary to popular belief, Lorem Ipsum is not simply random text. It
has roots in a piece of classical Latin literature from 45 BC, making
it over 2000 years old.
</p>
</Modal>
if You see both of the code snippets, you will realize that the first code snippet just feels natural and Maintainable while in the second snippet we are passing everything in props (except the main body) so if you want to change anything in the
header
orfooter
we have to pass more props to theModal
.but that won't be the case for Compound component.Now let's look at how that code actually works.
function Header({ children }) {
return <h1 className="header">{children}</h1>;
}
function BackDrop({ clicked, show }) {
return show ? <div onClick={clicked} className="backdrop" /> : null;
}
export function Footer({ children }) {
return <div className="action">{children}</div>;
}
export function Modal({ show, children, onClose }) {
return show ? (
<>
<BackDrop show={show} clicked={onClose} />
<div className="modal">{children}</div>
</>
) : null;
}
Modal.Header = Header;
Modal.Footer = Footer;
- now let's look at Each component one by one.
Header And Footer Component :-
- these two-component just accept
children
as a prop and put them into a container with style.
BackDrop :-
- this component is responsible for the backdrop behind the Modal.
Modal :-
Main Modal Component has only
Backdrop
anddiv
withchildren
. and it will be visible whenever theshow
prop is true.but the main magic happens Here.
Modal.Header = Header;
Modal.Footer = Footer;
Here
Modal
is just javascriptfunction
and in javascriptfunctions
are also made fromobject
.So here we just added two propertiesHeader and Footer
.Here is a live example.
In compound components, it is not necessary to write
<Modal.Header>
. The following code is also 100% correct.<Modal show={showModal} onClose={hideModalHandler}> <ModalHeader>stateless Compound Component</ModalHeader> <p className="contentContainer"> Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. </p> <ModalFooter> <div> <button onClick={hideModalHandler}>Cancel</button> <button onClick={hideModalHandler}>Confirm</button> </div> </ModalFooter> </Modal>
Most of the UI libraries use the
<Menu/>
and<MenuItem/>
patterns. but personally<Menu.item>
establishes a better relationship with<Menu/>
.
- (For Vue and svelte user: If look at the code shown above you will realize that with the help of the compound component you can also implement
named slots
in React 🤯)
exercise:-
- create the
Card
andMenu
components using the compound component and post the link into the comment section.
Stateful Compound Component.
Components like
Modal, Card
are considered as a stateless compound component. because compound components do not share any state between them.but you can also create compound components that share states between them. A famous example of that is the
Toggle
andAccordion
.let's create a
Toggle
component that will show one component when it is in on state and another component when it is in off state.- Here is the anatomy of our
Toggle
component.
<Toggle onToggle={toggleHandler}>
<Toggle.On>
<p>it is in ON state</p>
</Toggle.On>
<Toggle.Off>
<p>it is in OFF state</p>
</Toggle.Off>
<Toggle.ToggleButton />
</Toggle>
- there are two ways to create a
Toggle
component.
- create a React context that has toggle state and state updater and its children will consume that context.
- Another way is that you insert toggle state and state updater as props to each component in children.
we are going to implement the 2nd method here. the reason is that passing
state
andstate updater
as props will make our UI more cleaner.but how are we going to manipulate
children
and insertstate
andstate updater
? the answer is combination of React.cloneElement and React.Children.map.enough chit-chat. now let's look at the code.
import React, { useEffect, useState } from "react";
function ToggleButton({ toggleState, onClick }) {
return (
<button onClick={onClick}>{toggleState ? "On State" : "Off State"}</button>
);
}
function On({ toggleState, children }) {
return toggleState ? <>{children}</> : null;
}
function Off({ toggleState, children }) {
return toggleState ? null : <>{children}</>;
}
export function Toggle({ children, onToggle }) {
const [on, setOn] = useState(false);
function toggleStateHandler() {
setOn((toggle) => !toggle);
}
useEffect(() => {
onToggle(on);
}, [on, onToggle]);
return (
<div>
{React.Children.map(children, (child) =>
React.cloneElement(child, {
toggleState: on,
onClick: toggleStateHandler
})
)}
</div>
);
}
Toggle.On = On;
Toggle.Off = Off;
Toggle.ToggleButton = ToggleButton;
- Here
On
andOff
component acceptstoggleState
andchildren
as a prop and displays the component whenever it's appropriate. ToggleButton
also acceptstoggleState
andtoggleState updater
as a prop and you can change the state whenever you click the button.Now let's look at the main
Toggle
component. Here, we have declared theon
state which determines which component will be shown(Toggle.On
orToggle.Off
).onToggle
function will be called whenever we change theon
state with help of theuseEffect
hook and at last we are mapping our children by addingtoggleState
andtoggleStateChangeHandler
as props.Here is a live working example of it.
- If you want to see the example with
context api
check out this blog post by kent c dodds.
- I hope you learned something from this post and if you have any query feel free to drop a comment but for now that's it from me. have a good day 🤟🤟
exercise :-
- implement the
Accordion
component usingReact.cloneElement
. - implement this Todo example using
context api
. - post your code link in the comment section