239: Reusable Components, Composition, Headless APIs, and Controlled State
Learning outcomes
By the end of this lesson, you should be able to:
- explain and apply component responsibility in a realistic implementation;
- explain and apply composition in a realistic implementation;
- explain and apply controlled components in a realistic implementation;
- explain and apply uncontrolled components in a realistic implementation;
- explain and apply headless behavior in a realistic implementation.
Prerequisites and retrieval
This lesson assumes the earlier 01-06 foundation and the preceding lessons in this module. Before you read, retrieve one concrete example from a previous project where the same concern appeared. Perhaps a component became difficult to change, a prop list started encoding every possible layout, or a widget needed to coordinate its state with the rest of the page.
That retrieval is intentional. In a timed machine-coding exercise, the goal is not to recite terminology. The goal is to make a defensible design decision while keeping the implementation readable, accessible, testable, and straightforward to extend under interview pressure.
Terminology
- Component responsibility: A component should have one coherent reason to change. That does not mean every component must be tiny; it means its behavior and rendering should form a comprehensible contract.
- Composition: Prefer slots,
children, render props, or small subcomponents over giant prop lists that encode every possible layout variation. - Controlled components: A controlled component receives its value or state and callbacks from its parent. The parent owns the source of truth.
- Uncontrolled components: Local, uncontrolled state can reduce ceremony for an isolated widget whose state does not need to coordinate with surrounding features.
- Headless behavior: A complex widget can separate its behavior, state, and accessibility logic from its styling, allowing product code to control the resulting markup.
- Prop contracts: Use discriminated props when combinations are mutually exclusive. Avoid boolean explosions such as
isSmall,isCompact,isBordered,isCard, andisInlinewhen their interactions are unclear.
Mental model
Treat Reusable Components, Composition, Headless APIs, and Controlled State as a design problem with observable inputs, outputs, invariants, and failure modes. A reusable UI component should expose a stable behavioral contract while still giving product code appropriate control over content and presentation. Building a miniature design system before the requirement calls for one can consume the entire interview without making the solution safer.
A strong implementation makes assumptions visible, narrows uncertainty at boundaries, and leaves enough evidence to show why the design is safe. That evidence might be tests, types, constraints, metrics, or diagrams. The exact artifact matters less than being able to connect the design to an observable invariant.
A useful sequence for both interviews and production work is:
requirement -> constraints -> model -> implementation -> failure analysis -> verification
Do not move directly from a requirement to a library call. First state what must remain true. Then choose the mechanism that enforces that condition, and finally decide how you will verify it.
Deep dive
1. Component responsibility
The practical problem is not that a file has reached some particular line count. The problem is that one change now requires understanding unrelated logic, accessibility behavior, and rendering decisions at the same time. A component should therefore have one coherent reason to change.
Split a component when its logic, accessibility behavior, or rendering complexity becomes independently reusable. Do not split it merely because a file exceeds an arbitrary length; unnecessary boundaries can make a simple contract harder to follow.
Decision rule: Use component responsibility deliberately when it makes the contract or invariant easier to prove. If the split only reduces typing while hiding an assumption, the more explicit design is usually safer.
2. Composition
The familiar failure mode is a component with a prop for every possible arrangement: title text, title position, icon choice, footer mode, button alignment, and several flags that interact in ways no one can remember. Composition addresses that problem by letting the caller provide the parts that vary.
Prefer slots, children, render props, or small subcomponents over giant prop lists that encode every possible layout variation. This keeps the reusable component responsible for behavior and stable structure while product code supplies the content or presentation that is genuinely different.
Decision rule: Use composition when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.
3. Controlled components
When a widget's state must coordinate with filters, URL state, validation, analytics, or another component, keeping that state hidden inside the widget creates a second source of truth. A controlled component avoids that split: it receives its value or state plus callbacks, and the parent owns the source of truth.
The trade-off is ceremony. The parent must hold the state and respond correctly to updates, so a controlled API is not automatically the best API for every small interaction.
Decision rule: Use controlled components when shared ownership or coordination is part of the requirement and the contract or invariant becomes easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.
4. Uncontrolled components
For an isolated widget, forcing every keystroke or interaction through the parent can add ceremony without adding useful coordination. Uncontrolled state can be a good fit in that situation. The component owns its current state, while the caller still receives a deliberate initial value and relevant events.
The boundary matters. Do not hide state that another feature needs to read, reset, validate, or synchronize. If outside code needs to control the current value after initialization, the component is no longer merely an isolated uncontrolled widget.
Decision rule: Use uncontrolled components when local ownership makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.
5. Headless behavior
Styling and interaction logic do not have to live in the same abstraction. A complex widget can expose behavior, state, and accessibility information while allowing product code to choose the markup and visual treatment. This is headless behavior.
The pattern is useful when multiple presentations share difficult interaction rules. In a timed round, however, use it only when the interaction logic is actually complex. A headless abstraction introduced for a simple button can make the solution harder to read than the problem warrants.
Decision rule: Use headless behavior when separating behavior from presentation makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.
6. Prop contracts
Boolean props look inexpensive at first, but their combinations grow quickly. A component with isSmall, isCompact, isBordered, isCard, and isInline has many possible states, including combinations that were never designed or tested.
Use discriminated props when combinations are mutually exclusive. The type or runtime contract should make invalid combinations difficult to express rather than relying on callers to know which flags happen to work together. Avoid boolean explosions such as isSmall, isCompact, isBordered, isCard, and isInline when their interactions are unclear.
Decision rule: Use prop contracts deliberately when they make the contract or invariant easier to prove. If they only reduce typing while hiding an assumption, prefer the more explicit design.
Worked example
Consider a timed machine-coding exercise for a component or feature that must remain readable, accessible, testable, and easy to extend under interview pressure. Start by writing the requirement in one sentence. Then list the input and output contracts, and identify which of the concepts above owns each possible failure mode.
The key move is separation of concerns. Parsing and validation belong at the boundary. Domain rules belong in the domain or service layer. Persistence rules belong in the database or repository. Presentation rules belong in the client. Mixing those concerns can make a happy-path demo look shorter, but it makes edge cases and ownership much harder to reason about.
export async function handleRequest(input: unknown) {
const command = parseCommand(input);
const result = await service.execute(command);
return toHttpResponse(result);
}
The example is deliberately small. input is untrusted at the boundary, parseCommand establishes the command contract, service.execute applies the operation's rules, and toHttpResponse translates the result for the caller. The names stand in for those layers; they do not remove the need to define their behavior.
Walk through at least four cases: the normal path, an empty or missing value, a duplicate, retry, or concurrent path where relevant, and a dependency failure. For each case, state which layer detects the problem and what the caller observes. That exercise exposes whether the component or service has a clear owner for each invariant, which is the level of explanation expected in a senior code review or technical interview.
Production perspective
Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failure, stale clients, concurrent requests, malformed data, schema changes, and high-cardinality usage. Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after you can identify the bottleneck or risk with evidence.
When the topic involves an external dependency, define a timeout and cancellation strategy. When it involves persistence, define transaction and consistency expectations. When it involves user-visible state, define loading, empty, error, stale, and success states. When it involves security, assume the client can be modified and all network input is untrusted.
For reusable UI, accessibility is part of the behavior contract, not a styling detail. Keyboard interaction, focus behavior, semantic markup, and state announcements need to survive composition and alternate presentations. Likewise, a controlled API does not by itself make state correct: callers still need a clear update contract and tests for stale, invalid, or out-of-order values.
Guided lab
Build a reusable modal or select component with a small, coherent API. Support controlled state, keyboard interaction, and compositional content without introducing a giant configuration object.
Complete the lab with this discipline:
- Write the requirement and two non-requirements.
- List input, output, and error contracts before implementation.
- Implement the smallest correct vertical slice.
- Add at least one invalid-input test and one edge-case test.
- Instrument or inspect the behavior instead of guessing.
- Refactor one hidden assumption into an explicit type, constraint, function, or configuration.
- Explain one alternative design and why you did not choose it.
- Record a short “what would break at 10× scale?” note.
The lab should make the ownership decision visible. If the parent controls the open or selected state, show how updates flow back through callbacks. If a piece of state remains local, explain why no other feature needs to own it. If the markup is compositional or headless, test the keyboard and accessibility behavior independently of the visual arrangement.
Edge cases and failure modes
- Component responsibility: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes.
- Composition: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes.
- Controlled components: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes. Also test a parent update that arrives while the widget is interacting with the current value.
- Uncontrolled components: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes. Verify the initial value and events without assuming the caller can change current state directly.
- Headless behavior: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes. Verify that keyboard, focus, and accessibility behavior remain correct with alternate markup.
Common mistakes and debugging
- Solving the example instead of the requirement: a copied pattern can be syntactically correct but architecturally wrong.
- Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary”
anyvalues. - Testing only the happy path and therefore discovering contracts only after integration.
- Optimizing before measuring, or selecting a scalable mechanism without a scale requirement.
- Letting client-side behavior stand in for server-side authorization, validation, or persistence guarantees.
For debugging, reproduce the smallest failing case first. Inspect the actual value or execution plan, trace the boundary where the invariant first becomes false, and fix the owning layer rather than adding a downstream patch. In a component, that may mean checking the props and event sequence in the browser or test runner. In a request flow, inspect the boundary input, service result, and translated response separately.
Interview questions
- What problem does Component responsibility solve, and what trade-off or failure mode would make you choose a different approach?
- What problem does Composition solve, and what trade-off or failure mode would make you choose a different approach?
- What problem do Controlled components solve, and what trade-off or failure mode would make you choose a different approach?
- What problem do Uncontrolled components solve, and what trade-off or failure mode would make you choose a different approach?
- What problem does Headless behavior solve, and what trade-off or failure mode would make you choose a different approach?
Checkpoint
Without notes, explain Reusable Components, Composition, Headless APIs, and Controlled State to another developer in five minutes. Your explanation must include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code.
Mastery checklist
- I can define the core terms precisely.
- I can choose a design from requirements instead of from habit.
- I can implement and test the normal path and edge cases.
- I can explain the runtime, storage, or complexity cost.
- I can identify which layer owns validation, errors, and recovery.
- I can compare at least two reasonable alternatives.
- I can explain how the design changes at larger scale or stricter reliability.
