Why would a component publish a narrow imperative handle instead of handing callers its host node?
answer
- exporting the node exports your internals
- publish verbs, not structure
- commands only, never value setters
- define the call-before-output contract
basics
~20 sAn imperative handle is a small object of named operations a component publishes — focus, reset, scroll to a row. It keeps the component's internal nodes and structure private, so the markup can be rewritten without breaking any caller.
solid answer
~50 sHanding out the host node exports your internals: the caller can now read, style and mutate anything inside, and your next markup change becomes their breakage. An imperative handle inverts that. The component publishes a deliberately small set of named operations — `focus()`, `reset()`, `scrollToRow(id)` — and implements them against whatever nodes it happens to have today. The caller gets intent-level verbs instead of a node, and you keep the freedom to restructure. Two design rules keep a handle honest. First, only expose **commands whose result is not renderable output**: focusing, scrolling, selecting text, starting playback. Anything that changes what is displayed belongs in an input, or you have created a second source of truth. Second, decide and document what a call does before output exists or after teardown — no-op, queue, or fail loudly — because callers will do it.
go deeper
Know the two options: hand out the real node, or publish a few named operations. The named operations are the safer default because callers then depend on what you promised, not on your markup.
Explain what a leaked node exposes — structure, content, host-level mutation — and why that becomes accidental API. Give the rule that keeps a handle honest: commands only, never setters for displayed values.
Show the contract thinking: what a call does before output exists or after teardown, how the handle survives internals being re-created, and how you would answer a caller asking for an operation that should be an input.
Frame it as versioning cost. A published handle is API you must keep stable while internals change; a leaked node is API you never agreed to and cannot deprecate, which is why library boundaries get verbs.
Two escape hatches let code reach past a component's declarative surface. One is a **host-node reference**, which yields the real node the framework created. The other is an **imperative handle**: an object of named operations that a component deliberately publishes to whoever holds the reference to it. They look similar from the call site and are very different as API design. ## What you are choosing between | | The host node | A published handle | | --- | --- | --- | | What the caller receives | the real object, with every host capability | a fixed set of named operations | | What it reveals | your internal structure and markup | your intent-level verbs | | Cost of restructuring internals | callers break silently | nothing, if verbs are preserved | | Blast radius of caller code | anything the host permits | what you implemented | | What tests can assert | host details | behaviour you meant to promise | Handing out the node is not a small act of convenience: it publishes your internals as API. The day you wrap that field in another container, or swap the element for a different one, code you have never read stops working — and it was never advertised as depending on you. ## What the handle buys you - **Encapsulation with an escape hatch.** Callers can still do the imperative thing they genuinely need, without learning your tree. - **A vocabulary in domain terms.** "Bring row 42 into view" survives a rewrite; "the scrolling container is the second child" does not. - **A single place to enforce invariants.** Your implementation can refuse a nonsensical call, batch two host operations, or restore state afterwards. - **Something to type and document.** A named operation set is reviewable API; a leaked node is an open door. - **Fewer accidental dependencies.** Callers cannot come to rely on a class name, an attribute or a child's existence if they never see them. ## Designing one that stays honest 1. **Start from the caller's verb, not your node.** Collect the actual requests — "focus the first invalid field", "clear the entry", "play from the start" — and publish those. 2. **Admit only commands with no renderable result.** Focus, scroll, select, play, measure. If a call would change what the user sees, it belongs in an input so the declarative model stays the source of truth. 3. **Define the pre-output and post-teardown behaviour.** A caller will invoke an operation before any host object exists or after the component is gone. Pick one contract — silently ignore, queue until attached, or raise — and document it. 4. **Keep it stable under identity changes.** The handle must keep working after your internals are re-created, which means implementing operations against whatever the current nodes are rather than capturing one at setup. 5. **Keep it small enough to enumerate in a sentence.** A growing handle is usually a missing input or a missing output signal in disguise. ## The failure mode to watch for The common defect is a handle with **setters** on it. A caller that can push a value into the component now competes with the component's own inputs: the value exists in two places, and the last writer wins non-deterministically at the mercy of update ordering. The symptom is a field that reverts, or a component whose displayed value disagrees with what the parent believes it passed. The fix is always the same — move the value to an input and keep the handle for commands. A second, subtler one is the handle used to avoid threading an input down through intermediate components. That trades a visible chain of data for an invisible one, and it is the same argument as reaching for shared ancestor-provided values: a plumbing problem should be solved by plumbing, not by an imperative back channel. ## When the raw node is the right answer Publishing a handle is not always warranted. If the component is a thin adapter whose entire purpose is to give some other code a place to attach — a drawing surface, a slot for an imperative widget — the node *is* the contract, and dressing it in verbs adds indirection without adding encapsulation. Say so explicitly in that case, so nobody reads the exposure as an accident. Everywhere else, and especially in a shared library where you cannot see your callers, the narrow handle is the version you can still change next quarter.
- How do you keep an imperative handle from becoming a second source of truth?Admit only operations whose result is not renderable output — focus, scroll, select, play. The moment the handle can set a displayed value, that value lives both in the parent's inputs and in the component, and the two drift. If callers must set something, it is an input, not a handle operation.
- What does publishing a handle commit a library owner to?The same obligations as any public API: stable operation names and semantics, defined behaviour when called before output exists or after teardown, and idempotence where a caller might retry. The upside is that internals stay free to change, which is exactly what leaking a node gives away.
- A caller asks for an operation you think is wrong. How do you decide?Ask what it reads or writes. A command against the host with no declarative equivalent is a legitimate gap in the framework's model. A request to push in a value, or to read something so the caller can decide what to render, is a design problem — answer it with an input or an upward signal instead.
saying these in an interview costs you the question
- Publishes the host node itself and calls that an API
- Puts value setters on the handle, so state lives in two places
- Assumes an operation works before the component has produced output
- Adds a handle to avoid passing an input down the tree
- Captures one node at setup, so the handle breaks after internals are re-created
- Thinks a handle exempts the component from one-way data flow