| title | Component Composition Guide |
|---|---|
| filename | /docs/component-composition-guide.md |
| created | 2026-03-11 |
| modified | 2026-03-11 |
| version | 1.0.0 |
| related | /guidelines/overview-components.md, /docs/custom-hooks-guide.md |
Project: Ash Shaw Makeup Portfolio
Purpose: Best practices for composing and structuring React components
Created: March 11, 2026
Status: Active
Component composition is the art of building complex UIs from smaller, reusable pieces. This guide covers patterns, anti-patterns, and real-world examples from the Ash Shaw portfolio.
Key Principles:
- Single Responsibility Principle
- Composition over inheritance
- Props as configuration
- Extract when reusable or complex
- Keep components under 300 lines
- When to Split Components
- Composition Patterns
- Component Hierarchy
- Real-World Examples
- Anti-Patterns
- Refactoring Strategies
| Lines of Code | Action |
|---|---|
| 0-150 | ✅ Good size, no action needed |
| 150-300 | |
| 300-500 | 🔴 Should split into smaller components |
| 500+ | 🚨 Must split immediately |
Split when you see:
- Multiple
useStatecalls (5+) managing different concerns - Deeply nested JSX (4+ levels)
- Multiple
useEffecthooks for different purposes - Copy-pasted code blocks
- Difficulty understanding component purpose at a glance
Example from codebase:
// 🔴 BEFORE - 650 lines
export function StyleGuidePage() {
// 100+ lines of state
// 500+ lines of JSX
// Multiple sections doing different things
}
// ✅ AFTER - Extracted sections
export function StyleGuidePage() {
return (
<div>
<ColorsSection />
<TypographySection />
<ButtonsSection />
<IconsSection />
</div>
);
}When to use: Separate data logic from presentation
Container Component (Smart):
/**
* VideosPage - Container component
* Handles data fetching, filtering, and state management
*/
export function VideosPage() {
const [videos, setVideos] = useState([]);
const [activeCategories, setActiveCategories] = useState([]);
const [sortBy, setSortBy] = useState('recent');
const filteredVideos = useVideoFiltering({
videos,
activeCategories,
sortBy,
videoCategories,
});
return (
<VideoArchiveLayout
videos={filteredVideos}
categories={videoCategories}
activeCategories={activeCategories}
onCategoryToggle={handleCategoryToggle}
sortBy={sortBy}
onSortChange={setSortBy}
/>
);
}Presentational Component (Dumb):
/**
* VideoArchiveLayout - Presentational component
* Pure UI rendering with no business logic
*/
interface VideoArchiveLayoutProps {
videos: Video[];
categories: Category[];
activeCategories: string[];
onCategoryToggle: (slug: string) => void;
sortBy: string;
onSortChange: (sort: string) => void;
}
export function VideoArchiveLayout({
videos,
categories,
activeCategories,
onCategoryToggle,
sortBy,
onSortChange,
}: VideoArchiveLayoutProps) {
return (
<div className="video-archive">
<ArchiveFilters
categories={categories}
activeCategories={activeCategories}
onCategoryToggle={onCategoryToggle}
sortBy={sortBy}
onSortChange={onSortChange}
/>
<VideoGrid videos={videos} />
</div>
);
}Benefits:
- Easier to test (presentational components are pure)
- Easier to reuse UI components
- Clear separation of concerns
When to use: Related components that work together
Example: Accordion component
/**
* Accordion - Compound component pattern
*/
export function Accordion({ children }: { children: React.ReactNode }) {
const [openIndexes, setOpenIndexes] = useState<number[]>([]);
return (
<div className="accordion">
{React.Children.map(children, (child, index) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
isOpen: openIndexes.includes(index),
onToggle: () => {
setOpenIndexes((prev) =>
prev.includes(index)
? prev.filter((i) => i !== index)
: [...prev, index]
);
},
});
}
return child;
})}
</div>
);
}
export function AccordionItem({
title,
children,
isOpen,
onToggle,
}: AccordionItemProps) {
return (
<div className="accordion__item">
<button
className="accordion__question"
onClick={onToggle}
aria-expanded={isOpen}
>
{title}
</button>
{isOpen && (
<div className="accordion__answer">
{children}
</div>
)}
</div>
);
}
// Usage
<Accordion>
<AccordionItem title="Question 1">Answer 1</AccordionItem>
<AccordionItem title="Question 2">Answer 2</AccordionItem>
</Accordion>When to use: Share code between components using a prop whose value is a function
Example: Data fetching with render props
interface DataFetcherProps<T> {
url: string;
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode;
}
export function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => {
setData(data);
setLoading(false);
})
.catch((err) => {
setError(err);
setLoading(false);
});
}, [url]);
return <>{children(data, loading, error)}</>;
}
// Usage
<DataFetcher<Video[]> url="/api/videos">
{(videos, loading, error) => {
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <VideoGrid videos={videos || []} />;
}}
</DataFetcher>When to use: Add shared behavior to multiple components
Example: WithAuth HOC
/**
* withAuth - HOC that adds authentication
*/
export function withAuth<P extends object>(
Component: React.ComponentType<P>
) {
return function AuthenticatedComponent(props: P) {
const { isAuthenticated, user } = useAuth();
if (!isAuthenticated) {
return <LoginPrompt />;
}
return <Component {...props} user={user} />;
};
}
// Usage
const ProtectedPage = withAuth(DashboardPage);Note: Custom hooks are often preferred over HOCs in modern React.
When to use: Create flexible, reusable layouts
Example: Card component
interface CardProps {
header?: React.ReactNode;
children: React.ReactNode;
footer?: React.ReactNode;
className?: string;
}
export function Card({ header, children, footer, className }: CardProps) {
return (
<div className={`card ${className || ''}`}>
{header && <div className="card__header">{header}</div>}
<div className="card__body">{children}</div>
{footer && <div className="card__footer">{footer}</div>}
</div>
);
}
// Usage - Flexible composition
<Card
header={<h3>Portfolio Entry</h3>}
footer={<Button>View Details</Button>}
>
<img src={image} alt={title} />
<p>{description}</p>
</Card>App Component (Router)
├── Layout Components (Header, Footer)
├── Page Components (HomePage, AboutPage)
│ ├── Section Components (HeroSection, ContentSection)
│ │ ├── Block Components (PortfolioCard, BlogCard)
│ │ │ └── UI Components (Button, Image)
│ │ └── UI Components (Icon, Badge)
│ └── UI Components (Breadcrumbs, ScrollToTop)
└── Common Components (Modal, ErrorBoundary)
App.tsx
├── Header
│ ├── Logo
│ ├── Navigation
│ │ ├── PortfolioMegaMenu
│ │ │ ├── PortfolioCard (featured)
│ │ │ └── CategoryList
│ │ └── MobileMenu
│ │ ├── NavigationLinks
│ │ └── SocialLinks
│ └── ThemeSwitcher
├── HomePage
│ ├── Hero
│ ├── FeaturedSection
│ │ └── PortfolioCard (grid)
│ └── BlogPreviewSection
│ └── BlogCard (grid)
└── Footer
├── SocialLinks
└── FooterLinks
Before: Monolithic component (800+ lines)
export function EbookPage() {
// 15+ useState calls
// Touch gesture handling
// Keyboard navigation
// Fullscreen logic
// Page rendering
// TOC rendering
// Settings panel
// Progress tracking
// ... 700+ more lines
}After: Composed from smaller pieces
export function EbookPage() {
const ebookState = useEbookState(0);
useTouchGestures({
onSwipeLeft: ebookState.nextPage,
onSwipeRight: ebookState.prevPage,
});
useKeyboardNav({
onNext: ebookState.nextPage,
onPrev: ebookState.prevPage,
onClose: ebookState.toggleToc,
});
const fullscreen = useFullscreen();
return (
<div className="ebook">
<EbookHeader
currentPage={ebookState.currentPage}
totalPages={ebookState.totalPages}
onToggleToc={ebookState.toggleToc}
onToggleFullscreen={fullscreen.toggle}
/>
<EbookContent
page={ebookState.currentPage}
content={ebookState.currentContent}
/>
{ebookState.showToc && (
<EbookToc
chapters={ebookState.chapters}
onNavigate={ebookState.setPage}
/>
)}
</div>
);
}Benefits:
- Each hook/component has single responsibility
- Easier to test individual pieces
- Reusable hooks (useTouchGestures, useKeyboardNav, useFullscreen)
- Main component is now readable (under 100 lines)
Before: Inline lightbox (200+ lines in one file)
export function StickersPage() {
// State for stickers
// State for lightbox
// Lightbox JSX inline (200+ lines)
// Stickers grid JSX
}After: Extracted lightbox component
// /components/ui/StickerLightbox.tsx
export function StickerLightbox({
sticker,
onClose,
onPrev,
onNext,
}: StickerLightboxProps) {
useKeyboardNav({ onPrev, onNext, onClose });
return (
<div className="lightbox">
{/* Lightbox UI */}
</div>
);
}
// /components/pages/StickersPage.tsx
export function StickersPage() {
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
return (
<div>
<StickerGrid onOpenLightbox={setLightboxIndex} />
{lightboxIndex !== null && (
<StickerLightbox
sticker={stickers[lightboxIndex]}
onClose={() => setLightboxIndex(null)}
onPrev={handlePrev}
onNext={handleNext}
/>
)}
</div>
);
}Benefits:
- Lightbox is now reusable
- StickersPage is more focused
- Easier to test lightbox separately
Before: Duplicated code across 3 pages
// VideoCategoryPage.tsx (300 lines)
export function VideoCategoryPage() {
// Header
// Breadcrumbs
// Filters
// Grid
// FAQ
}
// VideoTagPage.tsx (300 lines) - 70% same code!
export function VideoTagPage() {
// Header
// Breadcrumbs
// Filters
// Grid
// FAQ
}
// BlogCategoryPage.tsx (300 lines) - 70% same code!After: Shared layout component
// /components/pages/shared/TaxonomyArchiveLayout.tsx
export function TaxonomyArchiveLayout({
breadcrumbs,
header,
filters,
children,
faqPageId,
}: TaxonomyArchiveLayoutProps) {
return (
<div className={mainClassName}>
<Breadcrumbs items={breadcrumbs} />
<div className={headerClassName}>{header}</div>
{filters && <ArchiveFilters {...filters} />}
<div className={contentClassName}>{children}</div>
<FaqSection pageId={faqPageId} />
</div>
);
}
// Usage - VideoCategoryPage (now 100 lines)
export function VideoCategoryPage() {
const filteredVideos = useVideoFiltering(/* ... */);
return (
<TaxonomyArchiveLayout
breadcrumbs={videoCategoryBreadcrumbs(category)}
header={<h1>{category.name} Videos</h1>}
filters={{
categories: videoCategories,
activeCategories,
onCategoryToggle: handleToggle,
}}
faqPageId="videos"
>
<VideoGrid videos={filteredVideos} />
</TaxonomyArchiveLayout>
);
}Benefits:
- DRY (Don't Repeat Yourself)
- Consistent layout across pages
- Single place to update structure
- Reduced code by 400+ lines
Problem: Passing props through many levels
// ❌ BAD
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} setUser={setUser} />;
}
function Layout({ user, setUser }) {
return <Header user={user} setUser={setUser} />;
}
function Header({ user, setUser }) {
return <UserMenu user={user} setUser={setUser} />;
}
function UserMenu({ user, setUser }) {
// Finally use the props 3 levels deep!
}Solution: Use Context or state management
// ✅ GOOD
const UserContext = React.createContext(null);
function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
<Layout />
</UserContext.Provider>
);
}
function UserMenu() {
const { user, setUser } = useContext(UserContext);
// Direct access, no prop drilling!
}Problem: Components that do everything
// ❌ BAD - 1000+ line component
export function DashboardPage() {
// Data fetching
// Authentication
// Form handling
// Chart rendering
// Table rendering
// Modal logic
// Navigation
// ... 900+ more lines
}Solution: Split into focused components
// ✅ GOOD
export function DashboardPage() {
return (
<DashboardLayout>
<DashboardHeader />
<StatsCards />
<ChartsSection />
<DataTable />
</DashboardLayout>
);
}Problem: Premature optimization
// ❌ BAD - Unnecessary memoization
function SimpleComponent({ name }) {
const greeting = useMemo(() => `Hello, ${name}!`, [name]);
const handleClick = useCallback(() => console.log(name), [name]);
return <div onClick={handleClick}>{greeting}</div>;
}Solution: Only memoize expensive computations
// ✅ GOOD
function SimpleComponent({ name }) {
const greeting = `Hello, ${name}!`; // No memo needed
const handleClick = () => console.log(name); // No callback needed
return <div onClick={handleClick}>{greeting}</div>;
}
// ✅ GOOD - Memoize expensive operations
function ExpensiveComponent({ data }) {
const processedData = useMemo(() => {
// Expensive filtering/sorting/mapping
return data.filter(/* ... */).sort(/* ... */).map(/* ... */);
}, [data]);
return <DataTable data={processedData} />;
}Problem: Defining components inside other components
// ❌ BAD
function ParentComponent() {
// This creates a NEW component on every render!
function ChildComponent({ text }) {
return <div>{text}</div>;
}
return <ChildComponent text="Hello" />;
}Solution: Define components outside
// ✅ GOOD
function ChildComponent({ text }) {
return <div>{text}</div>;
}
function ParentComponent() {
return <ChildComponent text="Hello" />;
}Identify repeating patterns:
// BEFORE
<section className="colors-section">
<h2>Colors</h2>
<div className="color-grid">
{/* Color swatches */}
</div>
</section>
<section className="typography-section">
<h2>Typography</h2>
<div className="type-list">
{/* Typography samples */}
</div>
</section>Extract to shared Section component:
// AFTER
function Section({ title, children, className }) {
return (
<section className={`style-guide-section ${className}`}>
<h2 className="section-title">{title}</h2>
<div className="section-content">{children}</div>
</section>
);
}
<Section title="Colors" className="colors-section">
<ColorGrid />
</Section>
<Section title="Typography" className="typography-section">
<TypeList />
</Section>Move stateful logic to hooks:
// BEFORE - Logic in component
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) return;
setLoading(true);
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => {
setResults(data);
setLoading(false);
});
}, [query]);
return (/* JSX */);
}
// AFTER - Logic in hook
function useSearch(query) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) return;
setLoading(true);
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => {
setResults(data);
setLoading(false);
});
}, [query]);
return { results, loading };
}
function SearchPage() {
const [query, setQuery] = useState('');
const { results, loading } = useSearch(query);
return (/* JSX */);
}Group related components:
// BEFORE - Separate unrelated components
<div className="modal">
<ModalHeader title="Settings" onClose={close} />
<ModalBody>Content</ModalBody>
<ModalFooter>
<Button onClick={save}>Save</Button>
</ModalFooter>
</div>
// AFTER - Compound component
<Modal>
<Modal.Header onClose={close}>Settings</Modal.Header>
<Modal.Body>Content</Modal.Body>
<Modal.Footer>
<Button onClick={save}>Save</Button>
</Modal.Footer>
</Modal>When composing components, ensure:
- Each component has single responsibility
- Components are under 300 lines
- Props interface is clear and typed
- No prop drilling (use context if needed)
- No inline component definitions
- Reusable logic extracted to hooks
- Repeated UI patterns extracted to components
- Clear component hierarchy
- Consistent naming conventions
- JSDoc documentation for complex components
Document Status: Active
Last Updated: March 11, 2026
Maintained By: Development Team