Your team wants one component codebase to drive a browser document, a native view tree and a server-side string. How would you split the runtime, and where does that abstraction leak?
answer
- bookkeeping ports, node vocabulary does not
- adapter: create, update, insert, remove, text
- the string host is the honest test
- share logic, fork presentation
basics
~20 sKeep identity, instance state, scheduling, difference-finding and commit ordering host-independent, and push a small primitive surface — create, update, insert, remove, text — into a per-host adapter. It leaks at property vocabulary, events, measurement and the component ecosystem.
solid answer
~50 sThe split that works is narrow at the bottom. The host-independent half owns component identity and instance state, the described-output structure, when an update runs, how differences are found, key matching and the commit's ordering. The host adapter implements primitives only: construct a node of a given type, apply a property change, insert before a sibling, remove, create and update text — plus optional hooks for adopting existing nodes and for measuring. A string host needs barely any of it, since it only ever appends. The leaks are where the hosts genuinely disagree: property vocabularies do not translate, event propagation and gestures differ so a delegation trick may not exist, layout and measurement models differ and some are asynchronous, and the component libraries, styling and accessibility tooling are all host-bound. So share state, data and logic, and expect presentation to fork.
go deeper
Know that the part of a framework that tracks components and finds changes is separate from the part that talks to a screen, which is why one framework can target more than one kind of output.
List what an adapter must implement — create, apply property changes, insert, remove, text — and explain why a string target needs almost none of it.
Show where the seams tear in practice: property vocabularies, event and gesture models, measurement timing, and the host-bound component and testing ecosystem around the runtime.
Own the decision. Define the portable line, keep the primitive surface narrow, forbid host conditionals in shared code, and price a second adapter as permanent team capacity rather than a project.
## What is genuinely host-independent A reconciler is mostly bookkeeping, and bookkeeping does not care what it is bookkeeping for: - **Component identity and instance state** — which instance occupies which position, and what it remembers across updates. - **The described-output structure** — a tree of *type plus properties plus children* descriptors, where the types are opaque strings or tokens the adapter interprets. - **Scheduling** — when an update pass runs, how updates coalesce, whether a pass can be interrupted. - **Difference-finding and key matching** — which instances survive, which are created, which move, which are torn down. - **Error propagation** to a catching ancestor, and **commit ordering** — teardown, mutations, references, pre-paint callbacks, deferred callbacks. None of that mentions a node. That is why one reconciler can drive several hosts at all. ## The primitive surface the adapter owns A host adapter is small by design. The recurring shape is: - **create a node** for a type, with an initial property set; - **apply a property change** from the previous property set to the next; - **create and update a text node** (or the host's equivalent); - **insert** a node into a parent, before a given sibling or at the end; - **remove** a node from its parent; - optionally **adopt an existing node** for a claim pass, **measure** a node for the pre-paint tier, and **bracket** a commit so the host can batch. Three hosts, three very different implementations: | Host | Node identity | Update path | Adopt existing | Measurement | |---|---|---|---|---| | A document tree | real nodes with attributes, styles and accessibility state | mutate in place | natural — markup already exists | synchronous read, at a cost the host defines | | A native view tree | typed view objects with platform-specific properties | mutate, often across a bridge or a batched queue | usually not available | often asynchronous, with its own layout engine | | A string for a response | none — output is append-only | no update, no remove, no move | not applicable | impossible | The string host is the useful sanity check on the design: if the abstraction only works when nodes can be mutated and re-found, it is not really host-independent. ## Where the abstraction leaks 1. **The property vocabulary does not translate.** A document has attributes, cascading styles, accessibility properties; a native tree has typed view properties and a layout system that is not the document's. A component that names a document property is already host-specific, even if the runtime under it is not. 2. **Events and gestures differ.** Propagation semantics vary, so a delegation strategy that works on one host may have no equivalent on another, and multi-touch gesture recognition has no document analogue at all. 3. **Layout and measurement.** A synchronous read-then-write before the frame is presented is expressible on some hosts and not on others; where measurement is asynchronous, the pre-paint tier cannot mean the same thing. 4. **Adoption.** A claim pass exists only where a host can produce a tree independently of the runtime. Most hosts cannot, so *create, update, adopt* collapses into *create, update*. 5. **Text and structural constraints.** Which parents accept which children, whether whitespace matters, and whether insertion order is guaranteed all vary — and these are exactly the rules a shared component will violate first. 6. **The ecosystem.** Component libraries, styling, animation, accessibility, testing and profiling are all host-bound. The reconciler ports; the buttons do not. ## How to decide - **Share downward, not upward.** Domain types, validation, data access, state shape, formatting and business rules port cleanly. Anything that names a property of a host does not. Put the split at that line and it holds. - **Refuse host conditionals inside components.** One component per host behind a shared interface beats one component with branches; the branches are where behaviour quietly diverges and neither host is tested for the other's path. - **Size the primitive surface deliberately.** Every extra primitive is a capability every future host must implement or fake. Narrow surfaces survive; wide ones become a dialect of the first host you wrote. - **Price the ownership.** A second adapter is not a one-time cost. It is a permanent obligation to keep two hosts' behaviour, tooling and performance characteristics in mind on every change — a team-shaped cost, not a technical one. - **Check whether you need the reconciler at all.** If the only second target is a string for a response, you need an append-only serialiser, not a portable host abstraction. ## The failure mode to name in an interview The abstraction rarely fails loudly. It fails as **a shared layer that quietly encodes one host's assumptions** — property names, a synchronous measurement, an event path — so the second host is permanently a port in progress, and every feature costs two implementations plus a translation. The judgment being tested is whether you can locate the line that actually ports, and defend keeping presentation on the other side of it.
- What actually ports between hosts in a real product, and what does not?Domain types, validation, data access, state shape, formatting and business rules port with no changes, and are usually most of the code. Anything that names a host property, relies on a host's layout system, or depends on its event or gesture model does not. Draw the boundary at the last layer that mentions no host, and keep presentation on the other side of it.
- Why forbid host conditionals inside shared components?Because each branch is code that only one host ever runs, so the other host's path is never exercised by that host's tests. Behaviour diverges silently, and the component becomes a place where two products are maintained in one file. One implementation per host behind a shared interface keeps each path honest and makes the divergence visible in the file layout.
- When would you decline to build a second adapter at all?When the second target is narrow enough to serve another way — a string response, an export, a preview — or when the shared layer would end up being logic you could share as a plain library instead. An adapter is permanent ownership: two sets of performance characteristics, tooling and host quirks on every change. Without a product reason that outlives the current quarter, it is not worth that.
saying these in an interview costs you the question
- Assumes components written for one host run unchanged on another
- Wants a wide primitive surface so every host feature is reachable
- Puts host conditionals inside shared components
- Expects a claim-existing pass to be available on every host
- Treats a second adapter as a one-off cost rather than ongoing ownership