Skip to main content
UncategorizedFrontend Modernization217 lines

Component Architecture

Component composition, compound components, render props, and slot patterns

Quick Summary16 lines
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 lines
Paste into your CLAUDE.md or agent config

Component 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 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.

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 UserCard that contains its own fetch call 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

Get CLI access →