Docs For AI
Architecture

Component Design

Composition patterns, prop design, and building reusable components

Component Design

Well-designed components are the foundation of maintainable UI. This covers patterns for building flexible, reusable components.

Component Categories

Presentational vs Container

// Presentational - pure UI, receives all data via props
function UserCard({ name, avatar, role, onEdit }: UserCardProps) {
  return (
    <div className="user-card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <span>{role}</span>
      <button onClick={onEdit}>Edit</button>
    </div>
  );
}

// Container - handles logic, passes data to presentational
function UserCardContainer({ userId }: { userId: string }) {
  const { data: user, isLoading } = useUser(userId);
  const { mutate: updateUser } = useUpdateUser();

  if (isLoading) return <UserCardSkeleton />;

  return (
    <UserCard
      name={user.name}
      avatar={user.avatar}
      role={user.role}
      onEdit={() => updateUser(user)}
    />
  );
}

Component Hierarchy

Component Types
├── Primitives
│   └── Button, Input, Text, Icon
├── Compounds
│   └── Card, Modal, Dropdown, Tabs
├── Features
│   └── UserProfile, ProductCard, SearchBar
└── Pages/Views
    └── Dashboard, Settings, Profile

Composition Patterns

Children Pattern

// Flexible composition via children
interface CardProps {
  children: React.ReactNode;
  className?: string;
}

function Card({ children, className }: CardProps) {
  return <div className={cn('card', className)}>{children}</div>;
}

function CardHeader({ children }: { children: React.ReactNode }) {
  return <div className="card-header">{children}</div>;
}

function CardBody({ children }: { children: React.ReactNode }) {
  return <div className="card-body">{children}</div>;
}

// Usage - flexible layout
<Card>
  <CardHeader>
    <h2>Title</h2>
    <Badge>New</Badge>
  </CardHeader>
  <CardBody>
    <p>Content goes here</p>
  </CardBody>
</Card>

Compound Components

// Context-based compound components
const TabsContext = createContext<TabsContextValue | null>(null);

function Tabs({ children, defaultValue, onChange }: TabsProps) {
  const [value, setValue] = useState(defaultValue);

  return (
    <TabsContext.Provider value={{ value, onChange: onChange ?? setValue }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function TabsList({ children }: { children: React.ReactNode }) {
  return <div className="tabs-list" role="tablist">{children}</div>;
}

function TabsTrigger({ value, children }: TabsTriggerProps) {
  const context = useContext(TabsContext);
  const isActive = context?.value === value;

  return (
    <button
      role="tab"
      aria-selected={isActive}
      onClick={() => context?.onChange(value)}
      className={cn('tab-trigger', isActive && 'active')}
    >
      {children}
    </button>
  );
}

function TabsContent({ value, children }: TabsContentProps) {
  const context = useContext(TabsContext);
  if (context?.value !== value) return null;
  return <div role="tabpanel">{children}</div>;
}

// Attach as static properties
Tabs.List = TabsList;
Tabs.Trigger = TabsTrigger;
Tabs.Content = TabsContent;

// Usage
<Tabs defaultValue="tab1">
  <Tabs.List>
    <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
    <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="tab1">Content 1</Tabs.Content>
  <Tabs.Content value="tab2">Content 2</Tabs.Content>
</Tabs>

Render Props

// Render prop for flexible rendering
interface MouseTrackerProps {
  render: (position: { x: number; y: number }) => React.ReactNode;
}

function MouseTracker({ render }: MouseTrackerProps) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);

  return <>{render(position)}</>;
}

// Usage
<MouseTracker render={({ x, y }) => (
  <div>Mouse is at ({x}, {y})</div>
)} />

// Alternative: children as function
interface MouseTrackerProps {
  children: (position: { x: number; y: number }) => React.ReactNode;
}

<MouseTracker>
  {({ x, y }) => <div>Mouse is at ({x}, {y})</div>}
</MouseTracker>

Slots Pattern (Vue-style in React)

interface CardSlots {
  header?: React.ReactNode;
  footer?: React.ReactNode;
  children: React.ReactNode;
}

function Card({ header, footer, children }: CardSlots) {
  return (
    <div className="card">
      {header && <div className="card-header">{header}</div>}
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}

// Usage
<Card
  header={<h2>Card Title</h2>}
  footer={<Button>Action</Button>}
>
  <p>Main content</p>
</Card>

Props Design

Props Interface Guidelines

// Good props interface
interface ButtonProps {
  // Required props first
  children: React.ReactNode;

  // Variants as union types
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
  size?: 'sm' | 'md' | 'lg';

  // Boolean props (default false)
  disabled?: boolean;
  loading?: boolean;

  // Callbacks with consistent naming
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
  onFocus?: (event: React.FocusEvent<HTMLButtonElement>) => void;

  // Allow extending native attributes
  className?: string;
  type?: 'button' | 'submit' | 'reset';
}

// Extend HTML attributes for full flexibility
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  loading?: boolean;
}

Polymorphic Components

type PolymorphicProps<E extends React.ElementType> = {
  as?: E;
  children: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<E>, 'as' | 'children'>;

function Text<E extends React.ElementType = 'span'>({
  as,
  children,
  ...props
}: PolymorphicProps<E>) {
  const Component = as || 'span';
  return <Component {...props}>{children}</Component>;
}

// Usage
<Text>Default span</Text>
<Text as="p">Paragraph</Text>
<Text as="h1">Heading</Text>
<Text as="a" href="/link">Link</Text>

Default Props

// Using default parameters (recommended)
function Button({
  variant = 'primary',
  size = 'md',
  disabled = false,
  children,
  ...props
}: ButtonProps) {
  return (
    <button
      className={cn('btn', `btn-${variant}`, `btn-${size}`)}
      disabled={disabled}
      {...props}
    >
      {children}
    </button>
  );
}

Component Patterns

Controlled vs Uncontrolled

// Uncontrolled - component manages its own state
function UncontrolledInput({ defaultValue, onChange }: Props) {
  const [value, setValue] = useState(defaultValue);

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
    onChange?.(e.target.value);
  };

  return <input value={value} onChange={handleChange} />;
}

// Controlled - parent manages state
function ControlledInput({ value, onChange }: Props) {
  return (
    <input
      value={value}
      onChange={(e) => onChange(e.target.value)}
    />
  );
}

// Hybrid - support both modes
function Input({ value: controlledValue, defaultValue, onChange }: Props) {
  const [internalValue, setInternalValue] = useState(defaultValue ?? '');
  const isControlled = controlledValue !== undefined;
  const value = isControlled ? controlledValue : internalValue;

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    if (!isControlled) {
      setInternalValue(e.target.value);
    }
    onChange?.(e.target.value);
  };

  return <input value={value} onChange={handleChange} />;
}

Higher-Order Components (HOC)

// HOC for adding loading state
function withLoading<P extends object>(
  WrappedComponent: React.ComponentType<P>
) {
  return function WithLoadingComponent({
    isLoading,
    ...props
  }: P & { isLoading: boolean }) {
    if (isLoading) {
      return <LoadingSpinner />;
    }
    return <WrappedComponent {...(props as P)} />;
  };
}

// Usage
const UserListWithLoading = withLoading(UserList);
<UserListWithLoading isLoading={loading} users={users} />

// Note: Prefer hooks over HOCs in modern React

Custom Hooks for Reuse

// Extract reusable logic into hooks
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);

  return { value, toggle, setTrue, setFalse };
}

// Usage in component
function Modal({ children }) {
  const { value: isOpen, setTrue: open, setFalse: close } = useToggle();

  return (
    <>
      <button onClick={open}>Open</button>
      {isOpen && (
        <div className="modal">
          <button onClick={close}>Close</button>
          {children}
        </div>
      )}
    </>
  );
}

Accessibility (A11y)

// Accessible button with proper ARIA
function IconButton({
  icon: Icon,
  label,
  onClick,
  disabled,
}: IconButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      aria-label={label}
      className="icon-button"
    >
      <Icon aria-hidden="true" />
    </button>
  );
}

// Accessible modal
function Modal({ isOpen, onClose, title, children }: ModalProps) {
  const titleId = useId();

  useEffect(() => {
    if (isOpen) {
      document.body.style.overflow = 'hidden';
    }
    return () => {
      document.body.style.overflow = '';
    };
  }, [isOpen]);

  if (!isOpen) return null;

  return createPortal(
    <div
      role="dialog"
      aria-modal="true"
      aria-labelledby={titleId}
      className="modal-overlay"
      onClick={(e) => e.target === e.currentTarget && onClose()}
    >
      <div className="modal-content">
        <h2 id={titleId}>{title}</h2>
        {children}
        <button onClick={onClose} aria-label="Close modal">
          ×
        </button>
      </div>
    </div>,
    document.body
  );
}

Best Practices

Component Design Guidelines

  1. Single responsibility - one component, one job
  2. Prefer composition over prop explosion
  3. Use TypeScript for prop interfaces
  4. Make components controlled when parent needs control
  5. Extract reusable logic into custom hooks
  6. Test components in isolation
  7. Document component API with examples
  8. Ensure accessibility (ARIA, keyboard navigation)

On this page