For a long-lived shared component library, how would you reason about choosing a template-based or render-function-based authoring model?
answer
- inventory the output first
- analyzability against expressiveness
- diagnostics, debugging, consumer build cost
- default plus a policed escape hatch
- internal choice behind a stable contract
basics
~20 sDecide from the shape of the output the library produces, not from taste: declarative markup rewards a template's analysis and diagnostics, while computed structure needs a function. Then design the escape hatch deliberately and police it.
solid answer
~50 sStart from the components, not the preference. Inventory what the library actually renders: if most of it is declarative markup with bindings, a compiled template buys build-time diagnostics, hoisting of the parts that never change, and a form that reviewers and designers can read. If a significant share of it computes its own structure — components chosen from configuration, recursive renderers, schema-driven forms — those fight a template's closed syntax and belong in functions. Then weigh what outlives the decision: diagnostics quality, how debuggable generated code is, what the compiler adds to every consumer's build, behaviour across more than one render host, and what a new hire must learn. The answer is usually not exclusive. **Default to one model, keep an explicit escape hatch for the computed-structure minority, and write down which components may use it** — the failure mode is not the choice, it is a boundary nobody polices.
go deeper
You are not expected to make this call, but know that the choice is a tradeoff between what tooling can analyse and what the author can express, not a question of which syntax looks nicer.
Be able to argue both sides concretely: build-time checks and hoisting on one hand, ordinary control flow and computed structure on the other, with an example component for each.
Bring the operational axes — debugging generated output, build time, diagnostics quality, a second render host — and propose a default with a narrow, enforced exception.
Own the reversibility analysis: what stays internal behind the component contract, what leaks into consumers' builds, and what signal would tell you the default was wrong.
This is a judgment question with no single right answer, and the interviewer is listening for the axes you weigh rather than the model you land on. ## Start from the output, not the syntax Inventory the library's components by the shape of what they render: - **Mostly-static markup with bindings** — buttons, cards, badges, layout primitives. A large fraction of their output can never change, which is precisely what a compiler exploits. - **Declarative with conditionals and lists** — tables, menus, navigation. Still expressible in a template's syntax, with an identity per item. - **Computed structure** — a control chosen from a configuration value, a recursive tree or outline renderer, a form generated from a schema, a component that wraps or transforms content it was handed. These read naturally as functions and awkwardly as templates. The ratio decides the default. A library whose long tail is the third category and who forces templates on it will produce a layer of indirection — configuration maps, dynamic-component indirection, deeply nested conditionals — that costs more than it saved. ## The axes that outlive the choice | axis | favours a compiled template | favours a render function | |---|---|---| | what tooling can prove | structure is analysable, constants hoistable | nothing is knowable until it runs | | diagnostics | a bad binding can fail the build | shape mistakes surface at runtime | | expressiveness | bounded by the syntax | bounded only by the language | | debugging | you step through generated code | the code you wrote is the code that runs | | build cost and complexity | a compiler in every consumer's pipeline | a lighter build step, or none | | reviewability | markup reads as structure | logic and structure interleave | | non-author contributors | designers can often follow it | usually not | | multiple hosts | the compiler must target each one | the description is host-agnostic | Two axes are chronically undervalued. **Debuggability**: when something goes wrong in compiled output, the stack points at code nobody wrote, and the quality of the mapping back to source is the difference between a ten-minute and a two-hour investigation. **Consumer build cost**: a library that requires a compiler imposes it on every application that uses it, including ones whose pipeline you do not control. ## Design the escape hatch on purpose Most mature libraries end up mixed, and that is fine if the mixture is intentional: 1. **Name the default** and the reason, in the contributing guide, so the choice is not re-argued per pull request. 2. **Define the exception** narrowly — for example, components whose child structure is derived from data rather than written literally. 3. **Make the boundary checkable.** A list of approved exceptions, or a lint rule, beats reviewer memory. 4. **Review the exception list periodically.** If it is growing steadily, the default was wrong for this library and the honest move is to change it rather than keep granting waivers. ## What I would not base it on Microbenchmark numbers alone; the claim that one model is simply faster is not true in general, and the difference will not dominate a real library's performance profile next to payload size, data fetching and layout cost. Nor personal familiarity alone — though team skills legitimately enter through the cost of training and hiring, which is a real number, not a taste. ## Reversibility Ask how expensive this decision is to unwind, because that calibrates how much analysis it deserves. The authoring model is **internal** if the library's public surface is its component contract — names, inputs, events, slots. A consumer who composes your components does not care how their insides were written, which means the model can be migrated component by component behind a stable API. What is not reversible is anything the choice leaks into the contract: a required compiler in the consumer's build, a syntax consumers must write when they extend a component, or a type-checking story they now depend on. Keep the leaks few and the decision stays a two-way door; let them multiply and you have made a one-way one by accident. ## What a strong answer sounds like It starts with an inventory rather than an opinion, names analyzability against expressiveness as the core tension, brings in at least two operational axes such as debuggability and consumer build cost, and ends with the mixed-model plan plus a policed boundary. It also says what would make it revisit the decision — the exception list growing, or a second host appearing.
- What would make you revisit the decision a year later?A steadily growing list of approved exceptions, which says the default does not match what the library actually renders. Also a second host to render into, a consumer whose build cannot take the compiler, or repeated incidents where compiled output made a defect expensive to trace.
- Is the authoring model part of the library's public contract?Only through its leaks. The contract is component names, inputs, events and insertion points, so insides can be migrated one component at a time. It becomes public when it forces a compiler into the consumer's build, a syntax on consumers who extend a component, or a type-checking story they rely on.
- How would you handle the minority of components whose structure is computed?Give them the escape hatch explicitly rather than contorting the template. Define the exception by a property of the component — its children are derived from data, not written literally — record which components qualify, and enforce it with a check rather than reviewer memory.
- Why is a performance benchmark a weak basis for this decision?Because the models are not separated by a general speed gap, and any gap is dwarfed in a real library by payload size, data fetching and layout cost. The compiler's advantage is that structural work can move to build time, which shows up on static-heavy components and barely at all on dynamic ones.
saying these in an interview costs you the question
- Picks the model from personal familiarity and calls it a standard
- Argues the choice on microbenchmark numbers alone
- Ignores that a required compiler lands in every consumer's build
- Forbids mixing rather than defining an escape hatch
- Treats an unpoliced convention as a decision
- Forgets that compiled output is what a debugger shows