React component design
A component is an API before it is markup. The props are the contract, and most component pain traces to a contract that configures behavior with flags instead of composing it with children.
Method
- Name the single responsibility first. A component does one thing: render a thing, own an interaction, or lay out other components. If you cannot state its job in one clause without "and", split it. Mixed responsibilities are the source of the twelve-prop component.
- Prefer composition over configuration. When a
variant,mode, orshowXprop starts branching the render tree, expose slots viachildrenor named render props instead. A<Card>that takes<Card.Header>and<Card.Body>scales; a<Card>withhasHeader,headerText,headerIcon,footerButtonsdoes not. - Decide controlled versus uncontrolled explicitly. Controlled means the
parent owns the value via
value+onChange; uncontrolled means the component owns it with an optionaldefaultValue. Pick one per piece of state and document it. Support both only through the standard pattern:value ?? internalValue. Silently switching between them mid-life is the React "controlled to uncontrolled" warning and a class of lost-edit bugs. - Keep the prop surface minimal and orthogonal. Every prop should be
independent; if two props are only valid in combination, model that as one
prop with a union type. Derive what you can from
childrenor context rather than asking for it. Fewer props means fewer illegal states. - Pass through the DOM props you did not consume. Spread
...restonto the root element and forwardrefwithforwardRefso callers can attacharia-*,className,onClick, and test ids without you enumerating them. A component that swallows unknown props forces a fork. - Default toward the common case. Set defaults so the zero-config usage is the right usage for most callers, and make the escape hatch explicit. Required props should be genuinely required, not defaulted to a guess.
Boundaries
- This is component API shape, not visual design; token and layout decisions belong to the design system.
- Global and server state placement is out of scope; see frontend-state.
- Framework primitives differ in Vue and Svelte, but composition-over-flags and explicit controlled state transfer directly.