# Component Architecture Patterns ## Overview Well-architected components are reusable, composable, and maintainable. This guide covers patterns for building flexible component APIs that scale across design systems. ## Compound Components Compound components share implicit state through React context, allowing flexible composition. ```tsx // Compound component pattern import * as React from 'react'; interface AccordionContextValue { openItems: Set; toggle: (id: string) => void; type: 'single' | 'multiple'; } const AccordionContext = React.createContext(null); function useAccordionContext() { const context = React.useContext(AccordionContext); if (!context) { throw new Error('Accordion components must be used within an Accordion'); } return context; } // Root component interface AccordionProps { children: React.ReactNode; type?: 'single' | 'multiple'; defaultOpen?: string[]; } function Accordion({ children, type = 'single', defaultOpen = [] }: AccordionProps) { const [openItems, setOpenItems] = React.useState>( new Set(defaultOpen) ); const toggle = React.useCallback( (id: string) => { setOpenItems((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { if (type === 'single') { next.clear(); } next.add(id); } return next; }); }, [type] ); return (
{children}
); } // Item component interface AccordionItemProps { children: React.ReactNode; id: string; } function AccordionItem({ children, id }: AccordionItemProps) { return (
{children}
); } // Trigger component function AccordionTrigger({ children }: { children: React.ReactNode }) { const { toggle, openItems } = useAccordionContext(); const { id } = useAccordionItemContext(); const isOpen = openItems.has(id); return ( ); } // Content component function AccordionContent({ children }: { children: React.ReactNode }) { const { openItems } = useAccordionContext(); const { id } = useAccordionItemContext(); const isOpen = openItems.has(id); if (!isOpen) return null; return
{children}
; } // Export compound component export const AccordionCompound = Object.assign(Accordion, { Item: AccordionItem, Trigger: AccordionTrigger, Content: AccordionContent, }); // Usage function Example() { return ( Is it accessible? Yes. It follows WAI-ARIA patterns. Is it styled? Yes. It uses Tailwind CSS. ); } ``` ## Polymorphic Components Polymorphic components can render as different HTML elements or other components. ```tsx // Polymorphic component with proper TypeScript support import * as React from 'react'; type AsProp = { as?: C; }; type PropsToOmit = keyof (AsProp & P); type PolymorphicComponentProp< C extends React.ElementType, Props = {} > = React.PropsWithChildren> & Omit, PropsToOmit>; type PolymorphicRef = React.ComponentPropsWithRef['ref']; type PolymorphicComponentPropWithRef< C extends React.ElementType, Props = {} > = PolymorphicComponentProp & { ref?: PolymorphicRef }; // Button component interface ButtonOwnProps { variant?: 'default' | 'outline' | 'ghost'; size?: 'sm' | 'md' | 'lg'; } type ButtonProps = PolymorphicComponentPropWithRef; const Button = React.forwardRef( ( { as, variant = 'default', size = 'md', className, children, ...props }: ButtonProps, ref?: PolymorphicRef ) => { const Component = as || 'button'; const variantClasses = { default: 'bg-primary text-primary-foreground hover:bg-primary/90', outline: 'border border-input bg-background hover:bg-accent', ghost: 'hover:bg-accent hover:text-accent-foreground', }; const sizeClasses = { sm: 'h-8 px-3 text-sm', md: 'h-10 px-4 text-sm', lg: 'h-12 px-6 text-base', }; return ( {children} ); } ); Button.displayName = 'Button'; // Usage function Example() { return ( <> {/* As button (default) */} {/* As anchor link */} {/* As Next.js Link */} ); } ``` ## Slot Pattern Slots allow users to replace default elements with custom implementations. ```tsx // Slot pattern for customizable components import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; interface ButtonProps extends React.ButtonHTMLAttributes { asChild?: boolean; variant?: 'default' | 'outline'; } const Button = React.forwardRef( ({ asChild = false, variant = 'default', className, ...props }, ref) => { const Comp = asChild ? Slot : 'button'; return ( ); } ); // Usage - Button styles applied to child element function Example() { return ( ); } ``` ## Headless Components Headless components provide behavior without styling, enabling complete visual customization. ```tsx // Headless toggle hook import * as React from 'react'; interface UseToggleProps { defaultPressed?: boolean; pressed?: boolean; onPressedChange?: (pressed: boolean) => void; } function useToggle({ defaultPressed = false, pressed: controlledPressed, onPressedChange, }: UseToggleProps = {}) { const [uncontrolledPressed, setUncontrolledPressed] = React.useState(defaultPressed); const isControlled = controlledPressed !== undefined; const pressed = isControlled ? controlledPressed : uncontrolledPressed; const toggle = React.useCallback(() => { if (!isControlled) { setUncontrolledPressed((prev) => !prev); } onPressedChange?.(!pressed); }, [isControlled, pressed, onPressedChange]); return { pressed, toggle, buttonProps: { role: 'switch' as const, 'aria-checked': pressed, onClick: toggle, }, }; } // Headless listbox hook interface UseListboxProps { items: T[]; defaultSelectedIndex?: number; onSelect?: (item: T, index: number) => void; } function useListbox({ items, defaultSelectedIndex = -1, onSelect, }: UseListboxProps) { const [selectedIndex, setSelectedIndex] = React.useState(defaultSelectedIndex); const [highlightedIndex, setHighlightedIndex] = React.useState(-1); const select = React.useCallback( (index: number) => { setSelectedIndex(index); onSelect?.(items[index], index); }, [items, onSelect] ); const handleKeyDown = React.useCallback( (event: React.KeyboardEvent) => { switch (event.key) { case 'ArrowDown': event.preventDefault(); setHighlightedIndex((prev) => prev < items.length - 1 ? prev + 1 : prev ); break; case 'ArrowUp': event.preventDefault(); setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : prev)); break; case 'Enter': case ' ': event.preventDefault(); if (highlightedIndex >= 0) { select(highlightedIndex); } break; case 'Home': event.preventDefault(); setHighlightedIndex(0); break; case 'End': event.preventDefault(); setHighlightedIndex(items.length - 1); break; } }, [items.length, highlightedIndex, select] ); return { selectedIndex, highlightedIndex, select, setHighlightedIndex, listboxProps: { role: 'listbox' as const, tabIndex: 0, onKeyDown: handleKeyDown, }, getOptionProps: (index: number) => ({ role: 'option' as const, 'aria-selected': index === selectedIndex, onClick: () => select(index), onMouseEnter: () => setHighlightedIndex(index), }), }; } ``` ## Variant System with CVA Class Variance Authority (CVA) provides type-safe variant management. ```tsx import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; // Define variants const badgeVariants = cva( // Base classes 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors', { variants: { variant: { default: 'border-transparent bg-primary text-primary-foreground', secondary: 'border-transparent bg-secondary text-secondary-foreground', destructive: 'border-transparent bg-destructive text-destructive-foreground', outline: 'text-foreground', success: 'border-transparent bg-green-500 text-white', warning: 'border-transparent bg-amber-500 text-white', }, size: { sm: 'text-xs px-2 py-0.5', md: 'text-sm px-2.5 py-0.5', lg: 'text-sm px-3 py-1', }, }, compoundVariants: [ // Outline variant with sizes { variant: 'outline', size: 'lg', className: 'border-2', }, ], defaultVariants: { variant: 'default', size: 'md', }, } ); // Component with variants interface BadgeProps extends React.HTMLAttributes, VariantProps {} function Badge({ className, variant, size, ...props }: BadgeProps) { return (
); } // Usage Active Error Draft ``` ## Responsive Variants ```tsx import { cva } from 'class-variance-authority'; // Responsive variant configuration const containerVariants = cva('mx-auto w-full px-4', { variants: { size: { sm: 'max-w-screen-sm', md: 'max-w-screen-md', lg: 'max-w-screen-lg', xl: 'max-w-screen-xl', full: 'max-w-full', }, padding: { none: 'px-0', sm: 'px-4 md:px-6', md: 'px-4 md:px-8 lg:px-12', lg: 'px-6 md:px-12 lg:px-20', }, }, defaultVariants: { size: 'lg', padding: 'md', }, }); // Responsive prop pattern interface ResponsiveValue { base?: T; sm?: T; md?: T; lg?: T; xl?: T; } function getResponsiveClasses( prop: T | ResponsiveValue | undefined, classMap: Record, responsiveClassMap: Record> ): string { if (!prop) return ''; if (typeof prop === 'string') { return classMap[prop]; } return Object.entries(prop) .map(([breakpoint, value]) => { if (breakpoint === 'base') { return classMap[value as T]; } return responsiveClassMap[breakpoint]?.[value as T]; }) .filter(Boolean) .join(' '); } ``` ## Composition Patterns ### Render Props ```tsx interface DataListProps { items: T[]; renderItem: (item: T, index: number) => React.ReactNode; renderEmpty?: () => React.ReactNode; keyExtractor: (item: T) => string; } function DataList({ items, renderItem, renderEmpty, keyExtractor, }: DataListProps) { if (items.length === 0 && renderEmpty) { return <>{renderEmpty()}; } return (
    {items.map((item, index) => (
  • {renderItem(item, index)}
  • ))}
); } // Usage user.id} renderItem={(user) => } renderEmpty={() => } /> ``` ### Children as Function ```tsx interface DisclosureProps { children: (props: { isOpen: boolean; toggle: () => void }) => React.ReactNode; defaultOpen?: boolean; } function Disclosure({ children, defaultOpen = false }: DisclosureProps) { const [isOpen, setIsOpen] = React.useState(defaultOpen); const toggle = () => setIsOpen((prev) => !prev); return <>{children({ isOpen, toggle })}; } // Usage {({ isOpen, toggle }) => ( <> {isOpen &&
Content
} )}
``` ## Best Practices 1. **Prefer Composition**: Build complex components from simple primitives 2. **Use Controlled/Uncontrolled Pattern**: Support both modes for flexibility 3. **Forward Refs**: Always forward refs to root elements 4. **Spread Props**: Allow custom props to pass through 5. **Provide Defaults**: Set sensible defaults for optional props 6. **Type Everything**: Use TypeScript for prop validation 7. **Document Variants**: Show all variant combinations in Storybook 8. **Test Accessibility**: Verify keyboard navigation and screen reader support ## Resources - [Radix UI Primitives](https://www.radix-ui.com/primitives) - [Headless UI](https://headlessui.com/) - [Class Variance Authority](https://cva.style/docs) - [React Aria](https://react-spectrum.adobe.com/react-aria/)