skip to content

Component Interface

How a Vue component declares and consumes its public contract: props, emitted events, v-model, fallthrough attributes, slots, registration and async loading. Core component-design ground.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

In Vue 3, what can you bind to `<component :is>`, and what happens to a tab panel's local state when the tab switches?

level: juniorimportance: must knowfreq 62%

answer

  1. one element, many possible types
  2. object, registered name, or tag
  3. type change means a new instance
  4. same type is patched, not recreated
  5. key forces a remount

basics

~20 s

Vue's <component :is> accepts an imported component object, a registered component name or an HTML tag name. Switching to a different component unmounts the old panel, so its local state is destroyed and returning mounts a fresh instance.

solid answer

~50 s

`<component :is="...">` is Vue's built-in meta element for rendering a type chosen at runtime. `is` can be the component definition itself (the normal choice in `<script setup>`, where imports are not registered), the name of a registered component, or a plain tag string such as `'a'`. When the value changes to a *different* component, Vue treats it as a different vnode type: the old panel is unmounted (its `onUnmounted` runs, its watchers stop) and the new one mounts from scratch with a fresh `setup` run. Coming back to the first tab therefore shows its initial state. If `is` stays on the *same* component while props change, the instance is reused and patched, so local state carries over; bind a `:key` to force a remount. To preserve switched-away panels, wrap the element in `<KeepAlive>` or lift the state into the parent.

code

vue · 12 lines
vue
<script setup lang="ts">
import { ref } from 'vue'
import UserPanel from './UserPanel.vue'

const userId = ref(1)
</script>

<template>
  <button @click="userId++">Next user</button>
  <!-- without :key the same UserPanel instance is patched and keeps its draft -->
  <component :is="UserPanel" :key="userId" :user-id="userId" />
</template>

go deeper

for a junior

Recall the three kinds of value is accepts and say plainly that switching unmounts the old panel, so its local state is lost.

for a middle

Explain the type-and-key comparison behind the unmount, and the reverse case where the same component is patched and keeps stale local state until you add a key.

for a senior

Choose deliberately between remount-by-key, lifting state, a store and a cache for each tab, and spot stale state copied from props during code review.

for a principal

Set a team convention for switchable panels: which ones may hold local state, which must be stateless views, and when caching instances is worth its memory.

## What `<component :is>` is `<component>` is a built-in **meta component** in Vue 3. It never appears in the DOM itself: it renders whatever type its `is` prop names, and renders a different type whenever that binding changes. It is the standard tool for a **tabbed settings page**, a wizard step, or any spot in a layout whose occupant is chosen from data. ## What `is` accepts | Value bound to `is` | What Vue renders | Typical use | |---|---|---| | An imported component object | That component | `<script setup>` code, where imports are used as variables | | A string naming a registered component | The component registered under that name | Options API code, globally registered components | | A string that is an HTML tag, such as `'a'` | A native element | A wrapper that is a link or a `span` | A few details matter in practice: - In `<script setup>`, imported components are **not registered by name**, so bind the object, not a string with its name. - Built-in components such as `Transition` can be passed as objects; passing them by name requires registering them first. - `v-model` on `<component>` always compiles to the component contract (`modelValue` plus `update:modelValue`), so it does not work when `is` resolves to a native `<input>` or `<select>`. ## What happens when the value changes When deciding whether it can reuse what is on screen, Vue compares the old and new vnode by **type** and **key**. When the bound component changes, the types differ, so nothing is reused: 1. The old panel is unmounted: `onBeforeUnmount` and `onUnmounted` run, its watchers and computed values are stopped, and its DOM is removed. 2. The new panel is created: its `setup` runs, its template renders, and `onMounted` fires. 3. Switching back repeats the same steps for the first panel, which yields a **brand-new instance** in its initial state. So a half-filled Privacy form is empty again after the user visits General and returns. That is not a bug; the old instance simply no longer exists. It is the single most common surprise with dynamic components, and interviewers like to ask it precisely because candidates who think of tabs as "hidden" get it wrong. ## Same component, different props The opposite case catches people too. If `is` keeps pointing at the same component, for example one `UserPanel` reused for several users, the type and key are unchanged, so Vue **patches** the existing instance with the new props. `setup` does not run again, and a local `ref` that was initialised from a prop keeps the old value. Two fixes: - bind a `:key` to the identity (`:key="userId"`), which makes the vnodes different and forces an unmount plus a fresh mount; - or stop copying the prop: derive the value with `computed`, or reset the local copy in a `watch` on the prop. ## Keeping state across switches When losing state is wrong for the product, there are three usual options: - **Wrap the element in `<KeepAlive>`**: switched-away instances are cached and deactivated instead of unmounted (its caching rules are a topic of their own). - **Lift the state up** into the parent that owns the tabs and pass it down as props, so each panel becomes a stateless view. - **Move it into a shared store** when other screens need the same draft. ## Example: a tabbed settings page ```vue <script setup lang="ts"> import { ref, type Component } from 'vue' import GeneralPanel from './GeneralPanel.vue' import PrivacyPanel from './PrivacyPanel.vue' const tabs: Record<string, Component> = { General: GeneralPanel, Privacy: PrivacyPanel, } const current = ref('General') </script> <template> <button v-for="(_, name) in tabs" :key="name" @click="current = name"> {{ name }} </button> <component :is="tabs[current]" /> </template> ``` `tabs` is a plain object: the component definitions never need to be reactive, and only the active key lives in a `ref`. Each click swaps the panel and unmounts the previous one, so any local state in `PrivacyPanel` starts over on the next visit.

  • In Vue 3, `<component :is>` keeps rendering the same `UserPanel` while its `userId` prop changes, and the old user's draft stays visible. Why, and what are the fixes?
    The vnode type and key are unchanged, so Vue patches the existing instance instead of creating a new one; `setup` does not run again, and a local `ref` copied from the prop keeps its first value. Bind `:key="userId"` to force a remount, or derive the value with `computed` or reset it in a `watch` on the prop.
  • Can Vue's `<component :is>` render a native element, and is there a catch?
    Yes: `is` can be a tag string, as in `:is="href ? 'a' : 'span'"`, which is handy for a link-or-text wrapper. The catch is `v-model`: on `<component>` it always compiles to `modelValue` and `update:modelValue`, so it does not bind a dynamically chosen native `<input>`; split it into a value binding and an event listener instead.

saying these in an interview costs you the question

  • Switching <component :is> back to a tab restores that tab's earlier state automatically.
  • A switched-away component is only hidden, like v-show, and stays mounted.
  • In script setup, :is must be given the component's name as a string.
  • Changing props on the same :is component recreates the instance and resets its state.
  • The dynamic component's setup function re-runs on every re-render.
open as a page

In a Vue 3 `<script setup>` component, how do you declare and emit a custom event, and how does the parent listen for it?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Call defineEmits(['confirm']) at the top level of script setup, keep the returned emit function and call emit('confirm', id). The parent listens with v-on, @confirm="handler", and the handler receives the extra arguments.

open as a page

In Vue 3, what happens to a class, style, id or @click listener a parent passes to a component that declares none of them?

level: juniorimportance: must knowfreq 62%

basics

~10 s

Undeclared attributes and v-on listeners are fallthrough attributes: Vue adds them to the component's single root element, merging class and style with the root's own values and running both listeners.

open as a page

In a Vue 3 `<script setup>` component, how do you declare props with defineProps, and how do runtime and type-based declarations differ?

level: juniorimportance: must knowfreq 76%

basics

~20 s

defineProps is a script setup compiler macro that declares props with either a runtime argument (a names array, or options with type, required, default and validator) or a TypeScript type argument compiled into runtime options; one call cannot take both.

open as a page

In Vue 3, what is the difference between registering a component with app.component() and importing it locally in `<script setup>`, and which should you prefer?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Vue's app.component() registers a component app-wide, so any template can use it without importing, but it always ships in the bundle. A local import in script setup is visible only in that file and tree-shakes cleanly, so it is the default.

open as a page

In Vue 3, how does a parent fill a child's named slots with `v-slot` or `#`, and when does a slot's fallback content render?

level: juniorimportance: must knowfreq 68%

basics

~20 s

Vue's child declares outlets such as <slot name="toolbar">, and the parent targets them with <template #toolbar> (short for v-slot:toolbar). Loose content goes to the default slot. Fallback content inside <slot> renders when the parent provides nothing usable.

open as a page

In Vue 3, what does `v-model="query"` on a custom component expand to, and what must the child component provide for it to work?

level: juniorimportance: must knowfreq 72%

basics

~10 s

On a component, v-model="query" expands to a modelValue prop plus an onUpdate:modelValue listener that assigns the payload back to query. The child declares the prop and emits update:modelValue, or uses defineModel() since Vue 3.4.

open as a page

In Vue 3, a page listens for @deleted on `<OrderList>`, but only its nested `<DeleteButton>` emits deleted; why does the handler never run?

level: middleimportance: must knowfreq 55%

basics

~20 s

Vue's emit only calls listeners the direct parent placed on that component, and events never bubble. OrderList must listen to DeleteButton and emit its own deleted, or the page reaches the value through provide/inject or a shared store.

open as a page

In Vue 3, how does `<DataTable>` expose each row to a parent's scoped cell slot, and why can't slot content read the table's own variables?

level: middleimportance: must knowfreq 58%

basics

~20 s

Vue slot content is compiled in the parent's scope, so it cannot see the child's variables. The DataTable passes data as attributes on the outlet, <slot name="cell" :row="row" />, and the parent receives them with <template #cell="{ row }">.

open as a page

In Vue 3.4+, what does defineModel() declare under the hood, and how does the ref it returns stay in sync with the parent?

level: middleimportance: must knowfreq 58%

basics

~20 s

defineModel() is a script setup macro that declares a model prop, a matching modifiers prop and an update:<name> event, and returns a ref: reads follow the prop, writes emit the update and keep a local value only when no parent v-model is bound.

open as a page

A Vue 3 `<UserCard>` edits its `user` object prop in place for inline renaming; what does Vue catch, what slips through, and how do you fix it?

level: seniorimportance: must knowfreq 60%

basics

~20 s

Reassigning a prop is blocked with a warning in development builds, but mutating a nested field such as props.user.name is not caught and silently changes the parent's object. Fix it with a copied local draft, a computed, or an emitted event.

open as a page

In Vue 3, how does `defineAsyncComponent` lazy-load a heavy chart panel, and what do its loadingComponent, delay, errorComponent and timeout options control?

level: middleimportance: should knowfreq 52%

basics

~20 s

Vue's defineAsyncComponent wraps a loader returning a promise, usually a dynamic import, and calls it only when first rendered. loadingComponent shows after delay (200 ms default); errorComponent shows on rejection or when an optional timeout passes.

open as a page

In a Vue 3 `<script setup>` tab switcher, why does binding `<component :is>` to a name string or a ref-held component misbehave?

level: middleimportance: should knowfreq 38%

basics

~20 s

A string for Vue's :is resolves only registered component names, and script setup imports are not registered, so it renders an unknown native element. A component stored in ref() becomes a reactive proxy, which Vue warns about and unwraps.

open as a page

A Vue 3 `<DeleteButton>` emits click from its root `<button>`, and the parent's @click handler runs twice per click; why, and what fixes it?

level: middleimportance: should knowfreq 45%

basics

~20 s

An undeclared click listener falls through to the root button as a native listener, and the child's emit('click') calls the same handler again. Declaring click in defineEmits removes it from $attrs, so only the emitted event reaches it.

open as a page

Why does Vue 3 warn about extraneous non-props attributes on a multi-root component, and how do you resolve the warning?

level: middleimportance: should knowfreq 42%

basics

~20 s

A multi-root, text or teleport root gives Vue no single element to put fallthrough attributes on, so it applies none and warns in development. Bind v-bind="$attrs" on the intended root, declare the prop or event, or set inheritAttrs: false.

open as a page

In a Vue 3 `<script setup>` component, why does logic derived from useAttrs() go stale, and what should you use instead?

level: middleimportance: should knowfreq 35%

basics

~20 s

useAttrs() returns an object that always holds the latest fallthrough attributes but is not reactive, so values copied in setup freeze and watchers cannot observe it. Declare a prop, read attrs during render, or use onUpdated.

open as a page

In Vue 3, how does Boolean casting resolve a prop typed Boolean when the parent omits it, writes it bare, or passes a string?

level: middleimportance: should knowfreq 50%

basics

~20 s

A prop whose type includes Boolean is cast: absent becomes false instead of undefined unless a default is declared, and a bare attribute or a value equal to the kebab-case prop name becomes true. Any other string, including "false", is passed as that string.

open as a page

In Vue 3, why must an Object or Array prop's default be a factory function, and when exactly is that default applied?

level: middleimportance: should knowfreq 55%

basics

~20 s

A prop's options are defined once per component, so a literal object or array default would be one shared reference that every instance could mutate. A factory returns a fresh value per instance and runs only when the resolved value is undefined.

open as a page

In Vue 3, what actually happens when a prop fails its type, required or validator check, and what can a validator see?

level: middleimportance: should knowfreq 42%

basics

~20 s

Vue only logs a console warning, and only in development builds; the value is still passed and the component renders. Production builds skip validation entirely. A validator receives the value and, since 3.4, the resolved props as a second argument.

open as a page

In Vue 3, how can a `<script setup>` component in TreeItem.vue render itself for nested folders, and what can override that self-reference?

level: middleimportance: should knowfreq 40%

basics

~20 s

A Vue SFC infers its name from its filename, so TreeItem.vue can render <TreeItem> in its own template without importing itself. An import with the same name takes priority; alias that import, and guard recursion with a condition on the children.

open as a page

In Vue 3, how do you render a card's footer wrapper only when the parent passed a footer slot, and when does `$slots.footer` mislead you?

level: middleimportance: should knowfreq 40%

basics

~20 s

Check the slot in Vue's template, <div v-if="$slots.footer" class="footer"><slot name="footer" /></div>, or use useSlots() in script setup. It tells you a footer fragment was passed, not that it renders anything, and it is not reactive in script.

open as a page

How do you build a Vue 3 `<DateRangePicker>` that supports `v-model:start` and `v-model:end` on the same component instance?

level: middleimportance: should knowfreq 50%

basics

~10 s

A v-model argument renames the prop and event, so v-model:start binds a start prop and update:start event. In the child, call defineModel('start') and defineModel('end'), or declare both props and both update events.

open as a page

In Vue 3, a defineAsyncComponent chart panel under a <Suspense> never shows its loadingComponent and a hung chunk never times out; why, and how do you harden its error path?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Vue async components are suspensible by default, so under a Suspense ancestor the boundary controls loading and the wrapper's loadingComponent, delay and timeout go unused. Set suspensible: false or rely on the fallback, then add timeout, errorComponent and a capped onError retry.

open as a page

A Vue 3 `<DeleteButton>` awaits a confirm dialog, then emits delete, but sometimes nothing happens and it cannot show a spinner; when should it take an onDelete callback prop instead?

level: seniorimportance: should knowfreq 35%

basics

~20 s

emit() returns nothing the child can await and is silently ignored once the component has unmounted. When the child needs the parent's result, such as a promise for a pending state, a function prop like onDelete fits better.

open as a page

A Vue 3 `<BaseInput>` renders an `<input>` inside a root `<label>`; why do a parent's placeholder and @focus miss the input, and how do you fix it?

level: seniorimportance: should knowfreq 48%

basics

~10 s

Fallthrough attributes land on the root label, where placeholder does nothing and focus never fires. Set inheritAttrs: false with defineOptions and bind v-bind="$attrs" on the input, deciding explicitly where class goes.

open as a page

In Vue 3, a page logs 'Failed to resolve component: UserBadge' and renders an empty unknown element; how do you work out which registration problem causes it?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Vue logs Failed to resolve component when a template tag matches no import, local registration or app-level registration. Check that the using file imports it, that the global registration is on this app, that the registered spelling matches, and that it is not a custom element.

open as a page

A Vue 3.4+ `<QuantityStepper>` built with defineModel shows 1 while the parent's bound ref stays undefined, or drifts from it; what causes it and how do you fix it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Either defineModel's default fills the child's prop while the parent's ref stays undefined, or the parent passes only :modelValue so the ref runs in local mode. Initialise the parent's value and bind the full v-model.

open as a page

In Vue 3, how does the object syntax of defineEmits validate an event payload, and what happens when validation fails?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

With the object syntax, each event maps to null or a function that receives the emit arguments and returns a boolean. A false result logs a development warning, but the event is still delivered, and production builds skip validators.

open as a page

In Vue 3, when does a template tag like `<UserCard>` or `<user-card>` resolve to a component, and why do in-DOM templates force kebab-case?

level: middleimportance: nice to knowfreq 35%

basics

~20 s

Vue matches a tag against registered names as written, camelized and capitalized, so a PascalCase registration answers to both <UserCard> and <user-card>. In-DOM templates are parsed by the browser first, which lowercases tags and ignores self-closing custom tags, so they need kebab-case and closing tags.

open as a page

How do you implement a custom `.capitalize` modifier for `v-model` on a Vue 3 component, so the parent always receives a capitalised value?

level: middleimportance: nice to knowfreq 32%

basics

~10 s

Modifiers reach the child as an object prop, modelModifiers for the default model. With defineModel, destructure [model, modifiers] and pass a set transformer that capitalises the value when modifiers.capitalize is true.

open as a page

showing 1–30 of 31