Mastering Composition in React

Composition is at the heart of great software. In React, it’s the principle that small, focused components can be combined to create flexible, reusable, and maintainable systems.
In this tutorial, we’ll explore composition in action by building a reusable modal system. Along the way, you’ll learn patterns that extend far beyond modals, patterns that apply to any React project and software in general.
Step 1: Build a Simple Modal Component
A good modal should focus on structure, not content. It shouldn’t care what’s inside; that’s for the parent to decide.
1type ModalProps = {
2 isOpen: boolean;
3 onClose: () => void;
4 children: React.ReactNode;
5};
6
7export default function Modal({ isOpen, onClose, children }: ModalProps) {
8 if (!isOpen) return null;
9
10 return (
11 <div className="fixed inset-0 bg-black/50 flex justify-center items-center z-50">
12 <div className="bg-white p-6 rounded shadow-lg max-w-md w-full">
13 {children}
14 <button
15 onClick={onClose}
16 className="mt-4 text-sm text-gray-500 hover:text-gray-700"
17 >
18 Close
19 </button>
20 </div>
21 </div>
22 );
23}
24Notice: Modal doesn’t decide what content goes inside. That’s composition; letting the parent component provide children.
Step 2: Render Dynamic Content
Because the modal accepts childrenYou can use it for any type of content, forms, confirmations, alerts, or info panels.
1<Modal isOpen={isOpen} onClose={closeModal}>
2 <h2 className="text-lg font-bold">Delete Item?</h2>
3 <p className="text-sm text-gray-600">This action cannot be undone.</p>
4 <button
5 onClick={handleDelete}
6 className="mt-4 bg-red-500 text-white px-4 py-2 rounded"
7 >
8 Delete
9 </button>
10</Modal>
11One component, multiple uses. That’s the essence of composition: reusable building blocks.
Step 3: Use Context for Global Modal Management
In larger apps, you might want modals to be triggered from anywhere without passing state around. Context lets us do that.
1import { createContext, useContext, useState, ReactNode } from "react";
2
3type ModalContextType = {
4 openModal: (content: ReactNode) => void;
5 closeModal: () => void;
6};
7
8const ModalContext = createContext<ModalContextType>({
9 openModal: () => {},
10 closeModal: () => {},
11});
12
13export function ModalProvider({ children }: { children: ReactNode }) {
14 const [content, setContent] = useState<ReactNode | null>(null);
15
16 const openModal = (newContent: ReactNode) => setContent(newContent);
17 const closeModal = () => setContent(null);
18
19 return (
20 <ModalContext.Provider value={{ openModal, closeModal }}>
21 {children}
22 <Modal isOpen={!!content} onClose={closeModal}>
23 {content}
24 </Modal>
25 </ModalContext.Provider>
26 );
27}
28
29export const useModal = () => useContext(ModalContext);
30Now, any component wrapped in ModalProvider can open a modal by calling openModal(content). No prop drilling. No tightly coupled state. Pure composition.
Step 4: Add Animations and Accessibility
With Tailwind, you can layer in transitions for smooth open/close behavior:
1<div className="fixed inset-0 bg-black/50 flex justify-center items-center z-50 transition-opacity duration-200">
2 <div className="bg-white p-6 rounded shadow-lg max-w-md w-full transform transition-transform duration-200 scale-95">
3 {children}
4 </div>
5</div>
6Step 5: Key Takeaways on Composition
- Single Responsibility: Each component does one thing well.
- Children & Props: Let components accept content rather than hardcoding it.
- Context & Providers: Decouple state management from UI.
- Layer Features: Animations, accessibility, and extra logic can be composed on top.
- Reusability & Flexibility: Small components combine to make scalable systems.
Composition isn’t just a React pattern — it’s a mindset for writing maintainable software.
Step 6: Challenge Yourself
Try extending this modal system:
- Add a confirmation modal that returns a promise.
- Make a form modal that submits data asynchronously.
- Animate different modal types differently.
Notice how you’re not rewriting Modal. You’re composing new behavior with the same building blocks. That’s the magic of good design.
Conclusion
Building reusable components like this is more than a coding exercise — it’s a lesson in thinking compositionally.
Small, focused pieces → combined thoughtfully → scalable, maintainable, flexible software.
Start simple, compose carefully, and you’ll find that building complex systems becomes easier, faster, and cleaner.