---
name: component-api-contract
description: "Reviews a UI component's public prop API for contract quality and recommends the composition pattern"
metadata:
  decimalai-skill-id: "368b112c-97d7-45bf-9346-e49d1912aaee"
  decimalai-version-id: "9c407244-32b9-4789-8c45-f816e8383f1d"
  decimalai-version: "1"
  decimalai-body-sha256: "0eeb77b94a5ed934a0d28d4a8d6f2fd96960fefd45e09d2494f86e93eb6f23b1"
  decimalai-name: "component-api-contract"
  decimalai-format: "1"
  decimalai-invocation: "model"
  decimalai-category: "frontend"
  decimalai-source: "https://app.decimal.ai/s/component-api-contract@1/SKILL.md"
---
# Component API Contract

Review the **public prop API** of a UI component for contract quality, then recommend the composition
pattern that fixes what you flag. This is a review, not a rewrite: name the defect and the target
pattern; do not reconstruct the whole component.

## When to Activate

Activate when the user asks to:
- Review the props / public API of a component before other teams depend on it
- Look at a component whose flags or props have grown and give a second opinion on the shape
- Check how a component takes state in and out from its consumers
- Sanity-check a component's prop signature or TypeScript props interface

Stay quiet on: explaining a concept in the abstract, writing a brand-new component from scratch, and
reviews that are not about the prop surface (styling, accessibility markup, test coverage).

## What to Flag

### 1. Boolean-prop proliferation

Two distinct smells, two different fixes.

- **Mutually-exclusive booleans → a single variant enum.** When several boolean props select *one
  look or one state at a time* (`primary`, `secondary`, `danger`, `outline`), collapse them into one
  string-union prop: `variant: 'primary' | 'secondary' | 'danger' | 'outline'`. Separate booleans are
  a bug surface — a consumer can pass `primary` **and** `danger` both true, which is meaningless, and
  every new style adds another flag instead of one more enum member.
- **A boolean paired with its own config props → compound components.** When a flag turns on a
  sub-region *and* that region needs its own configuration (`hasHeader` + `headerTitle` + `headerIcon`,
  `showFooter` + `footerButtons`), that is a slot being smuggled through props. Expose it as composition:
  `<Dialog><Dialog.Header/><Dialog.Body/><Dialog.Footer/></Dialog>`. The consumer places and fills the
  region directly instead of threading a bool plus N config props for each part.
- **Leave genuinely orthogonal booleans alone.** `disabled`, `loading`, `required`, `readOnly` can each
  be true independently and combine meaningfully. Do not fold independent flags into an enum — that is
  over-abstraction, not a fix.

Rule of thumb: 3+ booleans that are mutually exclusive → variant enum; a boolean that drags its own
configuration props behind it → compound slot.

### 2. Handler and prop naming consistency

- **Pair every state prop with a mirror-named change handler.** `value`/`onChange` (or
  `value`/`onValueChange`), `open`/`onOpenChange`, `checked`/`onCheckedChange`. The handler should name
  the prop it changes. Flag a state prop whose handler does not correspond to it (`selected` paired with
  `onChange`, or `value` paired with `onSelect`) and flag the same concept named differently across
  sibling components.
- **Event handlers are `on` + the event in PascalCase**, present tense: `onClick`, `onValueChange`,
  `onOpenChange`. Flag prop names like `handleChange`, `changeCallback`, or past-tense `onChanged`.
- **Pick one boolean-naming convention and hold it.** Either bare adjectives (`disabled`, `open`,
  `loading`) or a consistent prefix (`isDisabled`, `isOpen`, `isLoading`) — not a mix of `isOpen`,
  `disabled`, `hasError`, `shouldFocus` in one component.

### 3. Controlled vs uncontrolled state

- **A value prop with no change handler is a read-only trap.** `value` (or `checked`, `open`) without a
  matching `onChange`/`onCheckedChange`/`onOpenChange` freezes the component — the consumer can never
  update it. Flag it and add the handler (or switch to the uncontrolled `defaultValue`).
- **Accepting both `value` and `defaultValue` is ambiguous.** One selects controlled, the other
  uncontrolled; taking both leaves the mode undefined. Flag it — keep exactly one per render.
- **A component that flips between modes over its lifetime warns at runtime** (a `value` that goes from
  defined to `undefined`). The contract should commit to one mode.
- **Recommend the standard dual API:** `value` + `onChange` for the controlled path, `defaultValue` for
  the uncontrolled path, never both required at once. This is the shape consumers already expect from
  native inputs and mainstream libraries.

## How to Respond

For each issue, state (a) the specific defect in the current prop API and (b) the target pattern by name
— variant enum, compound components, or the controlled/uncontrolled dual API. When the API is already
clean, say so plainly and do not invent a defect. Recommend; do not hand back a full rewrite.
