FSD Frontend System Design

Part V β€” The Craft Β· Lesson 10.1

Component Design

In one lineDesign the props API before the implementation β€” the API is the part you can't change later.

Where you've seen itThe Button with isPrimary, isSmall, isLoading, hasIcon, iconRight, isFullWidth… each one added by someone in a hurry.

  • Time18 min
  • DiagramProps API surface + composition vs configuration
  • Chapter10 Β· Low Level Design

Diagram

diagram
CONFIGURATION vs COMPOSITION β€” the central choice

  CONFIGURATION (props explode)
    <Modal title="…" showClose hasFooter confirmText="…"
           onConfirm={…} cancelText="…" size="lg" icon="warn" />
    every new design need = a new prop, forever

  COMPOSITION (structure is passed in)
    <Modal>
      <Modal.Header>Delete invoice?</Modal.Header>
      <Modal.Body>This cannot be undone.</Modal.Body>
      <Modal.Footer>
        <Button variant="ghost">Cancel</Button>
        <Button variant="danger">Delete</Button>
      </Modal.Footer>
    </Modal>
    new design need = arrange the pieces differently
diagram
CONTROLLED vs UNCONTROLLED β€” support both

  UNCONTROLLED  <Input defaultValue="a" />
                component owns state Β· simple call site

  CONTROLLED    <Input value={v} onChange={setV} />
                parent owns state Β· needed for validation,
                                     dependent fields, reset

  Rule: if `value` is provided, it wins; otherwise fall back to
        internal state. One component, both modes.
diagram
THE API CHECKLIST β€” answer before writing JSX

  1. What does it OWN?           state it holds vs state passed in
  2. What does it EMIT?          onChange, onSelect, onDismiss
  3. What can be REPLACED?       children / slots / render props
  4. What's the DEFAULT?         works with zero props?
  5. How does it FAIL?           loading Β· empty Β· error Β· disabled
  6. How is it STYLED?           variant tokens, not free-form style
  7. Is it ACCESSIBLE by default? roles, keys, focus (8.2–8.4)

How it works

  1. Write the call site first. If the JSX you want to write is awkward, the API is wrong β€” change it before implementing.
  2. Prefer variant="danger" over booleans. Enumerated variants stay finite; booleans multiply into invalid combinations.
  3. Expose slots via children for anything a designer might rearrange.
  4. Support controlled and uncontrolled modes from one implementation.
  5. Bake in the four states β€” loading, empty, error, success β€” because consumers will otherwise reinvent each one badly.
jsx
function Input({ value, defaultValue, onChange, ...rest }) {
  const [internal, setInternal] = useState(defaultValue ?? '');
  const isControlled = value !== undefined;
  const current = isControlled ? value : internal;

  return <input {...rest} value={current} onChange={(e) => {
    if (!isControlled) setInternal(e.target.value);
    onChange?.(e.target.value);
  }} />;
}

Trade-offs

CompositionConfiguration
Flexible, no prop explosionSimple, one-line call sites
More markup at the call siteEvery variation needs code
Harder to enforce consistencyEasy to keep consistent

The healthy middle: configuration for the common 80% (<Button variant="primary">), composition for the rest (<Modal.Footer>). Component libraries that pick only one extreme end up either rigid or shapeless.

Interview angle

"Design a reusable Table component."

  • Start with the API out loud: columns config, data, onSort, renderCell for escape hatches, slots for empty and error states.
  • Keep sorting and pagination controlled so the parent can drive them from the URL (6.10).
  • Name the accessibility contract β€” real <table> semantics, scope on headers, sortable columns announcing aria-sort.

Recap

  • Write the call site before the implementation; the API outlives the internals.
  • Variants over booleans, slots over props, controlled and uncontrolled.
  • Loading, empty, error, success belong in the component, not in every consumer.