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!