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.
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 differentlyCONTROLLED 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.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
- Write the call site first. If the JSX you want to write is awkward, the API is wrong β change it before implementing.
- Prefer
variant="danger"over booleans. Enumerated variants stay finite; booleans multiply into invalid combinations. - Expose slots via children for anything a designer might rearrange.
- Support controlled and uncontrolled modes from one implementation.
- Bake in the four states β loading, empty, error, success β because consumers will otherwise reinvent each one badly.
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
| Composition | Configuration |
|---|---|
| Flexible, no prop explosion | Simple, one-line call sites |
| More markup at the call site | Every variation needs code |
| Harder to enforce consistency | Easy 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:
columnsconfig,data,onSort,renderCellfor 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,scopeon headers, sortable columns announcingaria-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.