A Vue 3 render function uses h('ButtonCounter') and h('input', { 'v-focus': true }) for a registered component and directive, but neither works: why, and how do you fix it?
answer
- what a string type means to h()
- templates resolve names for you
- resolveComponent and resolveDirective
- withDirectives wraps a vnode
- call them in setup or render
basics
~10 sA string passed to h() is always a native tag, and v-focus in props is just an attribute. Use resolveComponent('ButtonCounter') or an import for the component, and withDirectives(h('input'), [[focusDirective]]) with resolveDirective('focus') for the directive.
solid answer
~40 sThe template compiler normally emits `resolveComponent()`, `resolveDirective()` and `withDirectives()` calls for you; a hand-written render function gets none of them. `h('ButtonCounter')` therefore creates an unknown native element, and `'v-focus'` in props becomes a plain attribute. Fix it by passing the component definition: import it directly, or call `resolveComponent('ButtonCounter')` inside `setup()` or the render function. Apply the directive with `withDirectives(h('input'), [[focus]])`, where each entry is `[directive, value, argument, modifiers]`, resolving it with `resolveDirective('focus')` if it is only registered by name. A failed lookup warns 'Failed to resolve component' and returns the name string, so the unknown-element symptom persists. Prefer imports, because a typo then fails at build time.
code
ts · 13 linesimport { defineComponent, h, withDirectives, type Directive } from 'vue'
import ButtonCounter from './ButtonCounter.vue'
const vFocus: Directive<HTMLInputElement> = {
mounted: (el) => el.focus()
}
export default defineComponent({
setup() {
// imported definitions need no name resolution at all
return () => [h(ButtonCounter), withDirectives(h('input'), [[vFocus]])]
}
})go deeper
Recall that a string given to h() is a native tag and that components are passed as definitions, not names.
Explain what the template compiler emits for components and directives, and the withDirectives tuple shape of value, argument and modifiers.
Diagnose unknown-element and inert-directive symptoms from the dev warnings, and prefer direct imports over name resolution in your own code.
Decide how a component library exposes components and directives so render-function consumers can import rather than resolve them by name.
## The symptom A team ports a template to a render function. The template read: ```html <ButtonCounter /> <input v-focus /> ``` The render function they wrote: ```ts return () => [h('ButtonCounter'), h('input', { 'v-focus': true })] ``` In the browser the first node is an empty unknown element (the DOM lowercases the tag), the counter never appears, and the input is not focused. The second node even carries a stray `v-focus` attribute. ## What the template compiler was doing for you When Vue compiles a template, it recognizes that `ButtonCounter` is not a native tag and `v-focus` is a directive, and emits code for both: 1. It calls **`resolveComponent('ButtonCounter')`**, which looks the name up in the current component's local registrations and then in the app's global registrations (`app.component`), accepting PascalCase, camelCase and kebab-case variants. 2. It calls **`resolveDirective('focus')`** for the directive, again searching local then global registrations (`app.directive`). 3. It wraps the input's vnode in **`withDirectives(vnode, [[focus]])`**, which attaches the directive's hooks to that vnode. A hand-written render function gets none of this. To `h()`, a string type always means a **native element**, and a prop named `v-focus` is just an attribute. ## The fix ```ts import { defineComponent, h, resolveComponent, resolveDirective, withDirectives } from 'vue' export default defineComponent({ setup() { // must run inside setup() or the render function const ButtonCounter = resolveComponent('ButtonCounter') const focus = resolveDirective('focus') return () => [ h(ButtonCounter), focus ? withDirectives(h('input'), [[focus]]) : h('input') ] } }) ``` - Pass the **component definition**, not its name, to `h()`. When you can import it (`import ButtonCounter from './ButtonCounter.vue'`), do that and skip resolution entirely: the docs say you do not need `resolveComponent()` if you can import the component directly. - Apply directives with **`withDirectives(vnode, directives)`**, where each entry is `[directive, value, argument, modifiers]` and trailing items may be omitted. `v-pin:top.animate="200"` becomes `[[pin, 200, 'top', { animate: true }]]`. - Built-in components such as `Transition`, `KeepAlive` and `Teleport` are imported from `vue` for the same reason. ## The rules that trip people up | Rule | What goes wrong otherwise | |---|---| | Call `resolveComponent` / `resolveDirective` inside `setup()` or the render function | outside, there is no current instance; Vue warns that they can only be used in `render()` or `setup()` | | Handle a failed lookup | `resolveComponent` warns *Failed to resolve component: X* and returns the **name string**, so `h()` renders an unknown element; `resolveDirective` warns and returns `undefined` | | Use `withDirectives` only while rendering | called elsewhere, it warns that it can only be used inside render functions | | Expand `v-model` yourself on components | pass `modelValue` and `'onUpdate:modelValue'` props; there is no compiler to expand it | ## Local registration still works `resolveComponent()` checks the current component's **local** registrations before the app's global ones. A render-function component can therefore list `components: { ButtonCounter }` in its options and resolve the name, although at that point it already has the import in hand and could pass it to `h()` directly. The name lookup also accepts the component resolving itself by its own `name`, which is how a recursive component can refer to itself. ## Built-in directives Directives that ship with Vue are exported from `vue` and applied the same way. `v-show` becomes `withDirectives(h('div'), [[vShow, visible.value]])`, where the second tuple item is the directive's value. There is no `v-if` or `v-for` to apply: in a render function those are plain JavaScript, a conditional expression and an array `map()`. ## Diagnosing it quickly 1. In development, look for a *Failed to resolve* warning: it means the lookup ran but the name is not registered in that component's context. 2. No warning and an unknown element in the DOM means the name string went straight into `h()` with no lookup at all. 3. A directive attribute appearing in the DOM means it was passed as a prop instead of through `withDirectives`. ## Judgment Name-based resolution exists for components and directives that libraries register globally. In your own code, **direct imports are clearer**: they are statically analyzable, and a typo becomes an import error instead of a runtime warning.
- What does resolveComponent return when the name is not registered?It emits a development warning, 'Failed to resolve component: <name>', with a hint about `compilerOptions.isCustomElement`, and returns the name string itself. Passing that string to `h()` then creates a native element with that tag, which is why a misspelled or unregistered name still shows up as an unknown element.
- How do you express v-model on a child component in a render function?There is no compiler to expand it, so pass the two props it expands to yourself: `h(SomeInput, { modelValue: props.modelValue, 'onUpdate:modelValue': (v) => emit('update:modelValue', v) })`. The child sees exactly what a template's `v-model` would have given it.
- Why must resolveComponent be called inside setup() or the render function?It resolves against the current component's registrations and its app's global registry, so it needs a current instance. Called at module scope or in a later async callback there is none, and Vue warns that `resolveComponent` can only be used in `render()` or `setup()`.
saying these in an interview costs you the question
- Expects h() to look up registered components by their string name
- Passes directives as v-name props on the vnode
- Calls resolveComponent at module top level
- Assumes a failed resolveComponent returns undefined and throws
- Thinks a component must be registered before h() can render it