skip to content

A Vue 3 child's setup and onMounted run again on every parent re-render, wiping its state; what makes the renderer replace it instead of patching it?

level: seniorimportance: should knowfreq 32%

answer

  1. patch needs a matching vnode
  2. type identity, then key
  3. object created during render
  4. key that changes per render

basics

~20 s

Vue 3 patches a vnode only when old and new share the same type and key. A component object created during render, or a key that changes on every render, fails that test, so the renderer unmounts and remounts the child.

solid answer

~40 s

Vue 3's `patch` first checks `isSameVNodeType`: `type` must be the same reference and `key` must be equal. If not, it **unmounts** the old subtree and **mounts** a new one, so `setup()` and `onMounted` run again and local state is lost. The usual culprits are a `:key` computed per render (`Date.now()`, `Math.random()`, a freshly generated id), a `<component :is>` fed a component object built during render (calling `defineComponent` or `defineAsyncComponent` inside a function the template calls), or a wrapper whose element type switches. Also, `v-if`/`v-else` branches get compiler-generated keys, so the same component in both branches is a different vnode. To diagnose, log in `onMounted`/`onUnmounted` and inspect the parent's render; to fix, create component objects once and use stable keys.

code

vue · 18 lines
vue
<script setup lang="ts">
import { defineComponent, h, onUnmounted, ref } from 'vue'
import Counter from './Counter.vue'

const ticks = ref(0)
const id = setInterval(() => ticks.value++, 1000)
onUnmounted(() => clearInterval(id))

// Created once when setup runs: same reference on every render
const Wrapped = defineComponent({ setup: () => () => h(Counter) })
const rows = ref([{ id: 'a1', label: 'First' }])
</script>

<template>
  <p>{{ ticks }}</p>
  <component :is="Wrapped" />
  <Counter v-for="row in rows" :key="row.id" />
</template>

go deeper

for a junior

Recall that changing a component's key or type makes Vue create a new instance, losing its local state.

for a middle

Explain the same-type check on type reference and key, and list the template patterns that make either change per render.

for a senior

Diagnose remount loops from symptoms like repeated onMounted logs or lost input, trace them to the parent's render, and fix identity at the source.

for a principal

Set conventions that prevent identity churn, such as components defined once and keys from domain ids, and review dynamic component patterns against them.

## The rule the renderer applies Every time a Vue 3 component re-renders, its render function returns a new vnode tree and the renderer patches it against the old one. At each node the first question is whether old and new are **the same vnode type**: - `type` must be **the same reference** — the same tag string, or the very same component definition object. - `key` must be **equal** — both absent, or both the same value. If either differs, the renderer does not try to reconcile: it **unmounts** the old vnode (for a component, its unmount hooks run and its instance is discarded) and **mounts** the new one from scratch. For a component that means `setup()` runs again, `onMounted` fires again, and every `ref`, form value, scroll position and in-flight request tied to the old instance is gone. ## A snippet that remounts every second ```vue <script setup lang="ts"> import { defineComponent, h, onUnmounted, ref } from 'vue' import Counter from './Counter.vue' const ticks = ref(0) const id = setInterval(() => ticks.value++, 1000) onUnmounted(() => clearInterval(id)) // Bug: returns a new component object on every call function wrapped() { return defineComponent({ setup: () => () => h(Counter) }) } </script> <template> <p>{{ ticks }}</p> <component :is="wrapped()" /> <Counter :key="Date.now()" /> </template> ``` The parent reads `ticks`, so its render runs every second. Each run calls `wrapped()`, which builds a new object literal — `defineComponent` returns the options object it was given, so identity is new each time — and evaluates `Date.now()` to a new key. Both children fail the same-type check and remount every second. ## The usual causes | Cause | Why the check fails | |---|---| | `:key` from `Date.now()`, `Math.random()` or a new id per render | Key differs on every render | | `<component :is>` given an object built during render | Type is a new reference each time | | `defineAsyncComponent(...)` called inside a computed or function that re-runs | New wrapper component, new type | | A wrapper whose tag switches, e.g. `<component :is="tag">` from `div` to `section` | Element type differs, whole subtree remounts | | The same component in both `v-if` and `v-else` branches | The compiler gives each branch its own key | The last row surprises people moving from Vue 2: Vue 3's compiler injects a distinct key into each `v-if`/`v-else-if`/`v-else` branch, so toggling between two branches that render the same component is a replacement, not a patch. ## How to diagnose it 1. Confirm it is a remount, not just a re-render: log in `onMounted` and `onUnmounted` of the child. A re-render runs neither; a remount runs both. 2. Find what makes the parent re-render at that moment — the state its template read. 3. Inspect the child's vnode in that parent render: is its `:key` expression stable? Is the `:is` value, or the component used in a render function, the same object each time? 4. Check for conditional branches around the child that flip between `v-if` and `v-else`. 5. In a render function or JSX, look for components defined inline inside the render. ## How to fix it - Create component objects **once** — at module level in a separate file or `<script>` block, or once when `setup()` runs — and pass that reference to `:is`. - Hold a component chosen at runtime in a `shallowRef` or mark it with `markRaw`; Vue warns in development with "Vue received a Component that was made a reactive object" when you store one in a deep `ref`. - Derive keys from the data's own stable identity, not from time, randomness or position. - If both branches should share one instance, render the component once outside the condition and switch what it receives through props. ## A note on development builds During hot module replacement the renderer deliberately reports a hot-updated component as a different type, forcing a remount. A remount seen only after editing a file is that mechanism, not a bug in the parent. ## Why it matters beyond lost state - Remounting is far more expensive than patching: every DOM node below the child is destroyed and recreated. - Side effects in `setup()` repeat — data fetches, subscriptions, timers — which can look like duplicate network requests. - Child components below the remounted one are remounted too, so a single unstable key high in the tree can reset a whole panel.

  • Why does `<component :is>` driven by a computed that calls `defineAsyncComponent` remount more than expected?
    Each time the computed re-evaluates, `defineAsyncComponent` creates a new wrapper component, so the vnode type is a new reference and the old instance is unmounted. Create the async components once, for example in a map keyed by name, and let the computed pick from that map.
  • In Vue 3, how do you tell a re-render from a remount when debugging?
    A re-render runs the component's render function and its update hooks but not `setup()`; a remount runs unmount hooks on the old instance, then `setup()` and `onMounted` on a new one. Logging in `onMounted` and `onUnmounted` separates the two immediately.

Patching is like a stage crew redressing the same actor between scenes; if the script names a different actor, even one wearing the identical costume, the first actor leaves the stage and the new one starts from their first line.

saying these in an interview costs you the question

  • Blames Vue for re-running setup on every re-render of a component.
  • Believes a key only matters inside v-for lists.
  • Thinks defineComponent caches its result so repeated calls return one object.
  • Assumes the same component in v-if and v-else branches keeps its state in Vue 3.
  • Fixes the symptom by moving state to a global store instead of stabilizing identity.