Gustavo Santos

Components must compose

Composition is one of the best techniques that we can implement using React declative APIs. There is no reason to create God components.

I often see people writing components that take 10 or more properties. This week I saw a component that receives 17 properties that combine itself inside an huge component.

Although such kind of API is common on libraries of components, the internal implementation of such components are mostly like a façade. They are just a shell for many other components that compose together.

For example, when we design a class, it’s common to not pass 17 arguments into its constructor. Instead, we split such god class into many small classes that are able to compose and work together.

In my humble opinion, creating big domain components that receive 17 properties is a bad practice. As the same that creating a god class that receive 17 arguments is a bad practice.

To illustrate my opinion, lets design a modal component.

The design team has created a modal component that looks like this:

  • It could contain a title
  • It could contain a close button when on desktop viewport
  • It could contain a puller to drag the modal down and close it
  • It could contain a body in any form
  • It could contain a footer with buttons

If we start implementing this component without TDDing it, it easly could be something like this:

function Modal({
  title,
  hasPuller,
  hasCloseButton,
  children, // as a body
  confirmButton: { label: "Confirm", onClick: Function },
  cancelButton: { label: "Cancel", onClick: Function },
  // and so on...
}) {}

Soon or later we will need to control the property of showing or hidding the modal programatically, so we add one more property:

function Modal({
	open
	// the rest of all properties
}) {}

Or control the modal size:

function Modal({
  fullHeight,
  size: "small" | "medium" | "large",
  // the rest of all properties
}) {}

Or control the modal position:

function Modal({
  position: "top" | "center" | "bottom",
  // the rest of all properties
}) {}

You get the idea. Soon or later we will need to control the modal in many ways, and we will end up with a component that receives 17 properties or more.

As I already mentioned, this is a bad practice and we must try to avoid it. Front-end programming isn’t different from programming in general, and we must try to follow the same principles of good programming.

So, how can we avoid this? We can split the modal component into many small components that compose together.

function ModalTitle({ children }) {
	return (
		<div className="modal-title">
			<h2>{children}</h2>
		</div>
	);
}

function ModalCloseButton({ onClick, title = 'close' }) {
	return (
		<button className="modal-close-button" onClick={onClick} title={title}>
			<Icon />
		</button>
	);
}

export const Modal = {
	Title: ModalTitle,
	CloseButton: ModalCloseButton
	// and so on...
};

And so on.

Those components are easier to test, they mostly receive at most 3 or 5 properties. They are easier to read and easier to create test cases.

For example, before write a modal component that acts as the modal shell, we can start writing a test for it.

import { render, screen } from '@testing-library/react';
import { Modal } from './Modal';

it('returns null when isOpen is false', () => {
	render(<Modal isOpen={false} />);
	expect(screen.queryByRole('dialog')).toBeNull();
});

it('renders the modal when isOpen is true', () => {
	render(<Modal isOpen={true} />);
	expect(screen.getByRole('dialog')).toBeInTheDocument();
});

Surely by using TDD, we will end up with a higher quality component. The modal shell just control when it will render, it’s a simple task, with simpler tests and simpler production code.

But just splitting the huge modal component into smaller ones not solve the problem. Just splitting the huge modal will need us to use maybe a lot of components to create each modal where it needs to be rendered.

Stop to think for a while, you don’t need to do this and personally I think that you shouldn’t do this. Each modal must have a meaning, must exist to do something and we need to encode this information somehow. The common way to encode such thing is to give a name to that abstraction.

For example, we could create an account cancellation modal that is used to prompt the user if the user really wants to cancel its account.

function AccountCancelationModal({ isOpen, onConfirm, onClose }) {
	return (
		<Modal.Modal isOpen={isOpen}>
			<Modal.Header>
				<Modal.Title>You really want to cancel your account?</Modal.Title>
			</Modal.Header>
			<Modal.Body>
				<p>
					If you cancel your account, you will lose all your data and you will not be able to
					recover it.
				</p>
				<img src="image/of/a/sad-puppy.jpg" />
			</Modal.Body>
			<Modal.Footer style="stacked">
				<Modal.PrimaryAction onClick={onConfirm}>Yes, cancel my account</Modal.PrimaryAction>
				<Modal.SecondaryAction onClick={onClose}>
					No, I want to keep my account
				</Modal.SecondaryAction>
			</Modal.Footer>
		</Modal.Modal>
	);
}

Then we use the AccountCancelationModal component. It will read better, will be easier to test and easier to maintain. The design language will be kept in all modal variants and everyone will be happier.

If for any reason the product must produce a modal that is different form every other modal, the architecture is flexible enough to made the change without impacting all other modals.

Each modal will have its meaning.

This is why we should create components that compose and avoid god component.