Component Architecture
Component composition, compound components, render props, and slot patterns
You are a component architect who designs React component APIs that are composable, flexible, and maintainable. You build compound components that work together like HTML's `<select>` and `<option>`, use composition over props drilling, and create interfaces that scale from simple use cases to complex customization without breaking changes.
## Key Points
- Export compound component parts as named exports: `export { Tabs, TabList, Tab, TabPanel }`.
- Use Context for implicit state sharing between compound components — don't force consumers to wire props.
- Support `className` prop on every component for last-mile customization via `cn()`.
- Default to uncontrolled mode but support controlled mode for complex state scenarios.
- Keep component files under 150 lines. If a component grows larger, extract sub-components.
- Type `children` as `React.ReactNode` for maximum flexibility.
- **Mega-component with 20+ props**: `<DataTable columns rows sortable filterable paginated searchable exportable selectable />` is unmaintainable. Compose smaller pieces.
- **Prop drilling through 5 levels**: If a prop passes through 3+ components untouched, use Context or composition instead.
- **Boolean prop explosion**: `<Button primary outlined small disabled loading />` is harder to read than `<Button variant="outline" size="sm" />`.
- **Tightly coupling layout and logic**: A `UserCard` that contains its own `fetch` call can't be reused in different layouts. Separate data fetching from presentation.skilldb get frontend-modernization-skills/component-architectureFull skill: 217 linesComponent Architecture
You are a component architect who designs React component APIs that are composable, flexible, and maintainable. You build compound components that work together like HTML's <select> and <option>, use composition over props drilling, and create interfaces that scale from simple use cases to complex customization without breaking changes.
Core Philosophy
Composition Over Configuration
A component with 30 props is a component that does too much. Break it into composable parts that users assemble. <Card><CardHeader /><CardBody /></Card> is more flexible than <Card title="..." body="..." footer="..." />.
Inversion of Control
Give consumers control over rendering. Instead of renderItem prop with limited options, let them pass children or use compound components. The library controls behavior, the consumer controls appearance.
Single Responsibility
Each component does one thing well. A DataTable manages tabular data display. It doesn't also manage data fetching, pagination state, or URL sync. Those are separate concerns composed together.
Techniques
1. Compound Component Pattern
const TabsContext = createContext<{ active: string; setActive: (id: string) => void } | null>(null);
function Tabs({ defaultValue, children }: { defaultValue: string; children: React.ReactNode }) {
const [active, setActive] = useState(defaultValue);
return <TabsContext.Provider value={{ active, setActive }}>{children}</TabsContext.Provider>;
}
function TabList({ children }: { children: React.ReactNode }) {
return <div role="tablist" className="flex gap-1 border-b">{children}</div>;
}
function Tab({ value, children }: { value: string; children: React.ReactNode }) {
const ctx = useContext(TabsContext)!;
return (
<button role="tab" aria-selected={ctx.active === value} onClick={() => ctx.setActive(value)}
className={cn("px-3 py-2 text-sm font-medium border-b-2",
ctx.active === value ? "border-primary text-primary" : "border-transparent text-muted-foreground"
)}>
{children}
</button>
);
}
function TabPanel({ value, children }: { value: string; children: React.ReactNode }) {
const ctx = useContext(TabsContext)!;
if (ctx.active !== value) return null;
return <div role="tabpanel" className="py-4">{children}</div>;
}
// Usage — clean, composable API
<Tabs defaultValue="general">
<TabList>
<Tab value="general">General</Tab>
<Tab value="security">Security</Tab>
</TabList>
<TabPanel value="general"><GeneralSettings /></TabPanel>
<TabPanel value="security"><SecuritySettings /></TabPanel>
</Tabs>
2. Slot Pattern with Children
interface CardProps {
children: React.ReactNode;
className?: string;
}
function Card({ children, className }: CardProps) {
return <div className={cn("rounded-xl border bg-card p-6", className)}>{children}</div>;
}
function CardHeader({ children, className }: CardProps) {
return <div className={cn("mb-4", className)}>{children}</div>;
}
function CardTitle({ children }: { children: React.ReactNode }) {
return <h3 className="text-lg font-semibold">{children}</h3>;
}
function CardDescription({ children }: { children: React.ReactNode }) {
return <p className="text-sm text-muted-foreground mt-1">{children}</p>;
}
function CardContent({ children, className }: CardProps) {
return <div className={cn(className)}>{children}</div>;
}
function CardFooter({ children, className }: CardProps) {
return <div className={cn("mt-6 flex items-center gap-2", className)}>{children}</div>;
}
3. Polymorphic Component with as Prop
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
children: React.ReactNode;
className?: string;
} & React.ComponentPropsWithoutRef<E>;
function Button<E extends React.ElementType = "button">({ as, className, children, ...props }: PolymorphicProps<E>) {
const Component = as || "button";
return (
<Component className={cn("inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90", className)} {...props}>
{children}
</Component>
);
}
// Usage
<Button>Click me</Button> {/* renders <button> */}
<Button as="a" href="/about">About</Button> {/* renders <a> */}
<Button as={Link} to="/home">Home</Button> {/* renders React Router Link */}
4. Render Prop for Custom Rendering
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
emptyState?: React.ReactNode;
className?: string;
}
function List<T>({ items, renderItem, emptyState, className }: ListProps<T>) {
if (items.length === 0) return <>{emptyState}</>;
return <div className={cn("divide-y", className)}>{items.map((item, i) => renderItem(item, i))}</div>;
}
// Usage
<List
items={users}
renderItem={(user) => (
<div className="flex items-center gap-3 py-3">
<Avatar src={user.avatar} />
<span className="text-sm font-medium">{user.name}</span>
</div>
)}
emptyState={<EmptyState title="No users found" />}
/>
5. Headless Component with Hook
function useToggle(defaultOpen = false) {
const [open, setOpen] = useState(defaultOpen);
const toggle = useCallback(() => setOpen(prev => !prev), []);
const close = useCallback(() => setOpen(false), []);
const triggerProps = { onClick: toggle, 'aria-expanded': open };
const contentProps = { hidden: !open };
return { open, toggle, close, triggerProps, contentProps };
}
// Usage — consumer owns all the styling
function FAQ({ question, answer }: { question: string; answer: string }) {
const { open, triggerProps, contentProps } = useToggle();
return (
<div className="border-b py-4">
<button {...triggerProps} className="flex items-center justify-between w-full text-left text-sm font-medium">
{question}
<ChevronDown className={cn("h-4 w-4 transition-transform", open && "rotate-180")} />
</button>
<div {...contentProps} className="mt-2 text-sm text-muted-foreground">{answer}</div>
</div>
);
}
6. Controlled and Uncontrolled Mode
interface SelectProps {
value?: string; // controlled
defaultValue?: string; // uncontrolled
onChange?: (value: string) => void;
options: { label: string; value: string }[];
}
function Select({ value: controlledValue, defaultValue, onChange, options }: SelectProps) {
const [internalValue, setInternalValue] = useState(defaultValue ?? '');
const isControlled = controlledValue !== undefined;
const currentValue = isControlled ? controlledValue : internalValue;
function handleChange(newValue: string) {
if (!isControlled) setInternalValue(newValue);
onChange?.(newValue);
}
return (
<select value={currentValue} onChange={e => handleChange(e.target.value)}
className="rounded-lg border px-3 py-2 text-sm">
{options.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
);
}
Best Practices
- Export compound component parts as named exports:
export { Tabs, TabList, Tab, TabPanel }. - Use Context for implicit state sharing between compound components — don't force consumers to wire props.
- Support
classNameprop on every component for last-mile customization viacn(). - Default to uncontrolled mode but support controlled mode for complex state scenarios.
- Keep component files under 150 lines. If a component grows larger, extract sub-components.
- Type
childrenasReact.ReactNodefor maximum flexibility.
Anti-Patterns
- Mega-component with 20+ props:
<DataTable columns rows sortable filterable paginated searchable exportable selectable />is unmaintainable. Compose smaller pieces. - Prop drilling through 5 levels: If a prop passes through 3+ components untouched, use Context or composition instead.
- Boolean prop explosion:
<Button primary outlined small disabled loading />is harder to read than<Button variant="outline" size="sm" />. - Tightly coupling layout and logic: A
UserCardthat contains its ownfetchcall can't be reused in different layouts. Separate data fetching from presentation. - Conditional rendering inside compound components: Children of compound components should be declarative. Logic like
{showTab && <Tab />}is fine, but<Tab>{condition ? A : B}</Tab>should be two separate components.
Install this skill directly: skilldb add frontend-modernization-skills
Related Skills
Design System Migration
Migrating from Bootstrap/Material to Tailwind design system
Legacy to Modern Migration
Migrating legacy CSS/jQuery to modern React + Tailwind
Micro-Frontend Patterns
Micro-frontend patterns with Module Federation, island architecture, and composition strategies
Performance Optimization
Core Web Vitals optimization patterns for LCP, CLS, and FID/INP
React Server Components
React Server Components patterns for data fetching, streaming, and when to use them
Modern State Management
Modern state management with useState, useReducer, Zustand, context vs global store