skip to content

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.