Welcome to my first ever written article. It will be about showing you the usage of compound components through an example of a Modal Box. I used a CSS-in-JS library called Emotion for styling. For the sake of conciseness, the styling codes will be omitted. The goal is to see what compound components bring to the table. The code is available on my Github repository.
Something familiar
Take a glance at this code snippet:
<select>
<option value="value1">option a</option>
<option value="value2">option b</option>
<option value="value3">option c</option>
<option value="value4">option d</option>
</select>
When an <option>
is clicked, <select>
somehow knows about it. This is because a state is implicitly shared between <select>
and <option>
. Their awareness of this state allows them, when put together, to do a specific task. I find that we get a nice API out of it. Compound components give the ability to do the same with the help of React Context.
Context provides a way of sharing values down to the component tree implicitly. It means you don’t have to pass values from component to component using props. In certain cases, the use of props can be cumbersome; you can easily end up with a lot of components doing nothing with those values but passing them down to their children. With Context, you get direct access to the needed data. It makes Context a great candidate for implementing compound components.
They work together
This is the Modal in use:
// src/App.js
function App() {
return (
<Modal>
<ModalOpenButton>Open modal</ModalOpenButton>
<ModalContent title="Modal title here!" imageSrc="./forest.jpg">
<p>Modal Content there!</p>
</ModalContent>
</Modal>
);
}
The Modal
component is a Context provider that yields a boolean state which says if the Modal is open or not.
// src/modal.js
const ModalContext = React.createContext();
function Modal(props) {
const [isOpen, setIsOpen] = React.useState(false);
return <ModalContext.Provider value={[isOpen, setIsOpen]} {...props} />;
}
ModalOpenButton
consumes the state so that when clicked, the button it returns, set isOpen
to true
.
// src/modal.js
function ModalOpenButton({ children }) {
const [, setIsOpen] = React.useContext(ModalContext);
return <button onClick={() => setIsOpen(true)}>{children}</button>;
}
Then we have ModalContent
, also consuming the ModalContext
, that put contents (its children) in the Modal. It decides to render the Modal Box when isOpen
is true
otherwise returns null
.
// src/modal.js
function ModalContent({ children, title, imageSrc }) {
const [isOpen, setIsOpen] = React.useContext(ModalContext);
return isOpen ? (
<Overlay onClick={() => setIsOpen(false)}>
<div
css={{...}}
onClick={(e) => e.stopPropagation()}
>
<div css={{...}}>
<h2 css={{..}}>
{title}
</h2>
<ModalCloseButton />
</div>
<div css={{...}}>{children}</div>
</div>
</Overlay>
) : null;
}
Once the Modal is open there are two ways of closing it: clicking the Overlay
or the ModalCloseButton
. Overlay
is a styled component and ModalCloseButton
another ModalContext
consumer. They both set isOpen
to false
when clicked.
// src/modal.js
const Overlay = styled.div({...});
function ModalCloseButton() {
const [, setIsOpen] = React.useContext(ModalContext);
return (
<button
onClick={() => setIsOpen(false)}
css={{...}}
>
<img alt="" src={timeSVG} />
</button>
);
}
Conclusion
Here is the list of our compound components:
Modal
ModalContent
ModalOpenButton
ModalCloseButton
They are all sync around a common state each taking action to bring specific functionality. Put them apart and they will not be as useful. Now we can pop the modal!