---
name: component-contract
description: Design a UI component's public interface — props/slots/events, controlled vs. uncontrolled ownership, composition patterns, lifecycle contract, and usage documentation — before writing any implementation.
---

# Skill: component-contract

Load this skill when designing a new **shared component** — one that will be
used by multiple callers in the codebase. Do not load it for one-off
components local to a single page; the additional design overhead is not
warranted. Load `component-contract` when:

- Building a new component that will go into a shared UI library or component
  directory
- Refactoring a component that has grown multiple callers and its interface
  is inconsistent
- Reviewing a component's public interface before it is published or exported

The contract must be written **before** any implementation code. An interface
designed after the implementation reflects the implementation's needs, not the
caller's needs — it is the wrong order.

---

## Output rendering

Table — When presenting several items that share the same fields, render a Markdown table. Cap at ~5 columns; beyond that, switch to a per-item detail list. Right-align numeric columns.
Key–value / one record — For a single record's fields, use an aligned key: value list, not a two-row table.
Rationale / narrative — Use short ## headings and 2–3 sentence paragraphs. Don't force narrative into a table.

## The API-first principle

A component's public interface is its most durable artifact. The implementation
changes; the interface is what callers depend on. Once multiple callers
depend on a component's props, events, and slots, changing the interface is a
breaking change. An interface designed carelessly at the start becomes
technical debt proportional to its adoption.

The principle has two implications:
1. The interface is the deliverable of this design pass — not the implementation.
2. The interface should be the *minimum* that satisfies the callers' needs
   today, with room to extend without breaking.

---

## Controlled vs. uncontrolled ownership

**Uncontrolled component:** the component manages its own state internally.
The caller does not control the value; it just receives change notifications.

```jsx
// Uncontrolled: caller provides an initial value; component manages the rest
<InputField defaultValue="Initial text" onChange={handleChange} />
```

**Controlled component:** the caller owns the state. The component is a
pure rendering function — it displays what it's given and reports changes.

```jsx
// Controlled: caller owns the value; component is a display layer
<InputField value={controlledValue} onChange={setValue} />
```

**The rule for when to support both:** provide **uncontrolled by default**
(simpler for most callers), with a **controlled override** for callers that
need to manage state themselves (e.g., when the value must be derived from
external state, validated before setting, or synchronized with another control).

The most common pattern: accept both `value` (controlled) and `defaultValue`
(uncontrolled). When `value` is provided, operate in controlled mode; when
only `defaultValue` is provided, operate in uncontrolled mode.

---

## Props design rules

| Rule | Wrong | Right | Reason |
|---|---|---|---|
| Name props for what they ARE | `showModal={true}` | `isOpen={true}` | `show` is a verb (what to do); `isOpen` is a state (what it is) |
| Boolean props name the positive state | `disabledState` | `isDisabled` | Double negatives (`isNotDisabled`) are harder to reason about |
| Avoid encoding implementation | `useFlexLayout` | `layout="horizontal"` | The implementation detail leaks into the API; the caller shouldn't care how layout is achieved |
| Prefer composition over configuration | `<Button showIcon={true} iconName="check">` | `<Button><CheckIcon />Submit</Button>` | Multiple boolean props that add/configure content make the component harder to extend without changing the API |
| Consistent naming convention | Mixed `on_click`, `onClick`, `handleClick` | Always `on` + PascalCase event name | Consistency reduces cognitive load for callers |

---

## Slots and composition patterns

**Default slot (children):** the most flexible composition pattern. The caller
provides any content they need; the component provides the container and
behavior.

Use the default slot when: the component's job is behavior/wrapper (a modal,
a tooltip, a card), not content generation.

**Named slots:** when the component has multiple content regions with distinct
semantic roles (a card with `header`, `body`, and `footer`; a dialog with
`title` and `content`).

In JSX:
```jsx
<Card>
  <Card.Header>Card title</Card.Header>
  <Card.Body>Card content goes here.</Card.Body>
  <Card.Footer><Button>OK</Button></Card.Footer>
</Card>
```

In Vue / Web Components (explicit slot names):
```html
<card-component>
  <template slot="header">Card title</template>
  <template slot="body">Card content.</template>
</card-component>
```

**Render props:** a function-as-children pattern that gives the caller control
over rendering while the component provides data. Appropriate for components
that manage complex state and expose it to the caller for rendering.

Use render props when: the component manages state the caller needs to render
(e.g., a dropdown that tracks open/selected but lets the caller render the
options).

---

## Events contract

| Rule | Detail |
|---|---|
| Naming: `on` + PascalCase past-tense event | `onChange`, `onSubmit`, `onDismiss` — not `handle_click`, not `clicked` |
| Payload shape | Define the shape of the event payload in the contract. If it carries data (the new value, the selected item), document it explicitly. |
| What the component does NOT do after emitting | The component emits the event and stops. It does not optimistically update its own controlled state. The caller owns the state update. |
| Avoid emitting raw browser events | Wrap browser events in component-level events with meaningful names. `onChange` on a text field is a component event; it should not expose the browser's `InputEvent` object unless the caller genuinely needs browser-level access. |

---

## Lifecycle contract

Document what the component expects on mount, during its lifetime, and on
unmount:

- **On mount:** any async operations it initiates (data fetching, subscriptions,
  DOM measurements). What state is it in before those operations complete?
- **During lifetime:** any external dependencies it listens to (context,
  global stores, DOM resize observers). What happens when those change?
- **On unmount:** what it tears down (subscriptions, timers, event listeners).
  A component that does not clean up its subscriptions on unmount is a memory
  leak.
- **Strict mode double-invocation:** React 18+ strict mode double-invokes
  `useEffect` (and the equivalent in other frameworks) in development. Does
  the component survive a mount/unmount/mount cycle without observable side
  effects? If not, the lifecycle contract has a bug.

---

## Usage documentation template

Every shared component must ship a usage doc. Minimum viable format:

```markdown
## ComponentName

**Purpose:** One sentence describing what this component does and why it exists.

**When to use:** [conditions], not [counter-conditions].

### Props

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `value` | `string` | — | Controlled value. Provide with `onChange` for controlled mode. |
| `defaultValue` | `string` | `''` | Initial value for uncontrolled mode. |
| `isDisabled` | `boolean` | `false` | Disables the component; renders `aria-disabled`. |
| `onChange` | `(value: string) => void` | — | Called on every value change. |

### Events

| Name | Payload | When it fires |
|------|---------|--------------|
| `onChange` | `string` | Every time the user changes the value |
| `onBlur` | `FocusEvent` | When the component loses focus |

### Slots

| Name | Required | Description |
|------|----------|-------------|
| default | Yes | Content rendered inside the component |
| `label` | No | If provided, replaces the built-in label element |

### Accessibility contract

- Manages `aria-disabled` when `isDisabled` is true.
- Accepts focus via Tab; announces its label to screen readers via `<label>` association.
- Does not trap focus.

### Example

[Minimal complete usage example — the smallest correct usage, not a feature tour]
```

---

## Anti-patterns

| Anti-pattern | Problem | Fix |
|---|---|---|
| Prop drilling beyond 2 levels | Component requires callers to pass the same prop through 3+ levels of nesting | Extract a context (React Context, Vue provide/inject) to share the value without threading it through props |
| God component (more than one primary job) | A component renders a form AND handles data fetching AND manages a modal | Split into two components: one for UI (receives data via props), one for data management (renders the UI component) |
| Implicit global state mutation | Component writes to a global store directly rather than emitting an event | The component emits; the caller (or a coordinating layer) decides whether and how to update global state |
| Spreading all props on the root element | `<div {...props}>` passes unknown props to the DOM | Explicitly whitelist which props are valid for the root element; spread the rest only if a forwarded-refs pattern is intentional |
