skip to content

In Vue 3, when a parent component re-renders, what decides whether each child component re-renders as well?

level: middleimportance: must knowfreq 55%

answer

  1. not parent-driven by default
  2. props compared per key
  3. primitives stable, literals not
  4. declared emits are exempt

basics

~20 s

A Vue child re-renders only if its own tracked state changed or the parent hands it changed props or dynamic slots. Props are compared per key by identity, so primitives stay stable while fresh objects, arrays or functions count as changed.

solid answer

~50 s

When a parent re-renders, Vue checks every child component vnode before updating it. The child updates if a prop value differs by identity (`!==`, with `style` objects compared by value), if the set of props changed, if it receives dynamic slot content (slots under `v-if` or `v-for`), or if a runtime directive or transition sits on the component. Otherwise Vue keeps the child's existing render. So props stability is the lever: pass primitives or references that survive re-renders. An inline `:options="{ dense: compact }"` or `:rows="rows.filter(...)"` builds a new object on every parent render and forces the child to update; a `computed` keeps the same reference until its inputs change. Listeners for events the child declares in `emits` are excluded from the check, and a child still re-renders on its own when state it read changes.

code

vue · 27 lines
vue
<script setup lang="ts">
import { computed, onUnmounted, ref } from 'vue'
import ReportTable from './ReportTable.vue'

interface Row { id: number; visible: boolean; name: string }

const rows = ref<Row[]>([])
const compact = ref(false)
const clock = ref(Date.now())
const timer = setInterval(() => (clock.value = Date.now()), 1000)
onUnmounted(() => clearInterval(timer))

// Stable: same array until rows change.
const visibleRows = computed(() => rows.value.filter(r => r.visible))
// Stable: same object until compact changes.
const tableOptions = computed(() => ({ dense: compact.value }))
</script>

<template>
  <p>{{ new Date(clock).toLocaleTimeString() }}</p>

  <!-- Unstable: a new array and a new object on every clock tick. -->
  <ReportTable :rows="rows.filter(r => r.visible)" :options="{ dense: compact }" />

  <!-- Stable: the same references across ticks, so this table is skipped. -->
  <ReportTable :rows="visibleRows" :options="tableOptions" />
</template>

go deeper

for a junior

Recall that a Vue child updates when its props or its own state change, not simply because its parent re-rendered.

for a middle

Explain the check: per-key identity comparison, dynamic slots, directives on the component, and the declared-emits exemption.

for a senior

Spot unstable props in review - inline literals, filter or map calls, undeclared events - and fix them with computed values and primitive props.

for a principal

Decide where stable props are worth enforcing: lists and heavy children, not every component, and make it a review norm there.

## Two separate reasons a child re-renders In Vue 3 every component has its own **render effect**. It re-runs for exactly two reasons: 1. **Its own dependencies changed.** Reactive state the child read during its last render - its own refs, a store, a nested field of an object prop - was mutated. 2. **The parent's update decided it must.** When the parent re-renders, it produces a new vnode for the child and the renderer asks whether that child needs updating. The second path is where props stability matters. A parent re-rendering does **not** automatically re-render its children; the renderer checks first. ## What the check looks at The renderer's internal check (`shouldUpdateComponent` in the runtime source) forces an update when: - a **prop value changed**: compared per key with `!==`, except `style`, whose objects are compared by value; - the **number of props** changed, for components bound with a spread such as `v-bind="obj"`; - the component receives **dynamic slots**, meaning slot content under `v-if` or `v-for` that may differ between renders; - a **runtime directive** or a **transition** is attached to the component vnode. One exemption matters in practice: a listener for an event the child **declares in `emits`** is skipped, so a fresh inline handler for a declared event does not count as a changed prop. An undeclared listener falls through as an ordinary attribute and does count. ## Stable and unstable props | Passed as | Same value next render? | Effect on the child | |---|---|---| | `:count="n"` (primitive) | yes, if `n` is unchanged | skipped | | `:user="user"` (same reactive object) | yes, same proxy | skipped by the parent's check | | `:options="{ dense: compact }"` | no, a new object | updated every parent render | | `:rows="rows.filter(r => r.visible)"` | no, a new array | updated every parent render | | `:rows="visibleRows"` (a `computed`) | yes, until its inputs change | skipped | | `@select="pick(item.id)"`, `select` declared in `emits` | new function, but exempt | skipped | ## Why this is not the 'parent renders, children render' model In Vue, a parent that re-renders because a clock ticked does not re-render a child whose props are the same primitives and references as before. Conversely, a child can re-render **without** its parent: if it read `user.name` through an object prop and that field is mutated, the child's own render effect tracked it and re-runs, while the parent - which only passed the reference - does nothing. ## Slots and the check Slots add one more case. Content passed to a child through a slot is compiled into a function that the **child** calls during its own render. Two consequences follow: - For **stable slots** - plain slot content with no `v-if` or `v-for` around the slot itself - the parent's check does not force the child to update. If the slot content reads parent state that changes, that read was tracked by the child's render effect, so the child updates for that reason alone. - For **dynamic slots** - a slot rendered conditionally or in a loop - the compiler marks the child vnode, and the renderer forces the child to update on every parent re-render, because the set of slots may have changed. So a child that receives a conditionally rendered slot cannot be skipped by props stability alone; if that child is expensive, restructure so the condition lives inside the slot content rather than around it. ## Practical rules - **Derive values in `computed`**, not inline in the template, when they are passed as object or array props. - **Pass the smallest primitive the child needs** - a boolean instead of an id to compare against - so most children see an unchanged value. - **Declare emitted events** with `defineEmits`; besides documenting the component, it keeps inline listeners out of the update check. - **Do not over-apply it.** For a handful of cheap children, an extra update costs microseconds; the lever pays off in lists and heavy children.

  • You pass the same reactive object as a prop and mutate one of its nested fields. Does the child update?
    The parent's check sees the same reference and does not force an update. But if the child's render read that field through the prop, it tracked it, so the child's own render effect re-runs. The update comes from the child's dependency, not from the parent.
  • Why doesn't a fresh inline @select handler inside a v-for force every row to update?
    Vue's child-update check skips listener props for events the child declares in `emits`, so a new function for a declared event is not treated as a changed prop. If `select` were undeclared, the listener would be an ordinary fallthrough attribute and a new function on each render would force the update.

saying these in an interview costs you the question

  • A Vue child component re-renders whenever its parent re-renders.
  • Vue deep-compares every object prop before deciding to update a child.
  • An inline object literal prop is free because its contents are the same.
  • A computed passed as a prop creates a new array on every parent render.
  • A child can never update from a nested change to an object prop it received.