As we build more complex user interfaces in Next.js, mastering component patterns becomes crucial for creating maintainable, scalable, and reusable code. Two powerful patterns that stand out are Composition and Slots. These techniques allow us to build flexible components by defining how they can be extended and customized by their parent components.
Composition is the fundamental principle of building complex components by combining simpler, independent components. Instead of creating one monolithic component, we break it down into smaller, focused pieces that can be reused and assembled in various ways. This aligns perfectly with the React component model.
Consider a Card component. A card might typically contain a header, a body, and a footer. Instead of hardcoding these sections within the Card component itself, we can design it to accept them as children.
function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}
function CardHeader({ children }) {
return <div className="card-header">{children}</div>;
}
function CardBody({ children }) {
return <div className="card-body">{children}</div>;
}
function CardFooter({ children }) {
return <div className="card-footer">{children}</div>;
}
// Usage
<Card>
<CardHeader>Card Title</CardHeader>
<CardBody>
<p>This is the content of the card.</p>
</CardBody>
<CardFooter>Read More</CardFooter>
</Card>In this example, the Card component doesn't know or care about the specific content of its header, body, or footer. It simply renders whatever is passed to it via the children prop. This makes the Card component highly reusable and adaptable.
graph TD;
Card --> CardHeader;
Card --> CardBody;
Card --> CardFooter;
While children is excellent for general content, sometimes you need to provide specific areas within a component where different types of content can be placed. This is where the concept of slots comes in. Slots allow you to define named placeholders within a component, and parent components can then render content into those specific slots.
The most common way to implement slots in React (and by extension, Next.js) is by passing specific props that represent those slots.
Let's refactor our Card component to use slots for its header, body, and footer.
function Card({ header, body, footer }) {
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
{body && <div className="card-body">{body}</div>}
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// Usage
<Card
header={<h2>Card Title</h2>}
body={<p>This is the content of the card.</p>}
footer={<button>Read More</button>}
/>In this slot-based approach, the Card component explicitly defines header, body, and footer props. The parent component then passes the JSX for each slot directly to these props. This provides more explicit control over where content is rendered and makes the component's structure clearer.
graph TD;
Parent -->|header: JSX| Card;
Parent -->|body: JSX| Card;
Parent -->|footer: JSX| Card;
Card --> HeaderSlot;
Card --> BodySlot;
Card --> FooterSlot;
Often, the most effective component designs combine both composition and slots. You might have a higher-level layout component that uses composition for its overall structure, but exposes slots within its sub-components for customization.
For example, a Modal component might be composed of a ModalHeader, ModalBody, and ModalFooter. The Modal itself could accept children for general content, but expose specific slots for its internal sections.
function Modal({ title, content, actions }) {
return (
<div className="modal">
<div className="modal-header">{title}</div>
<div className="modal-body">{content}</div>
<div className="modal-footer">{actions}</div>
</div>
);
}
// Usage
<Modal
title={<h3>Welcome!</h3>}
content={<p>Please review our terms.</p>}
actions={<button>Accept</button>}
/>This approach offers a good balance between flexibility and structure. The Modal component is responsible for its overall layout and behavior, while the parent component provides the specific content for each designated area.
By leveraging composition and slots, you can build highly modular and adaptable UI components that are easier to understand, test, and extend. This is a cornerstone of building robust applications with Next.js and React.