A Vue 3 component logs from onUpdated, but the log fires on unrelated state changes and stays silent when a child's own state changes: why?
answer
- one render effect per component
- tracked reads during render
- a child re-renders on its own
- parent patches child only if props changed
basics
~10 sonUpdated fires when this component's own render effect re-runs, which happens when anything its render read changes, related or not. A child's internal state re-renders only the child, so the parent's onUpdated stays silent.
solid answer
~40 sIn Vue 3 each component has its own render effect that tracks the reactive values read while rendering. Any change to any of them, a clock ref, a `v-model` draft, a prop, an injected value, queues one re-render and one `onUpdated`, whether or not it relates to what you meant to log. Conversely, updates are per component: when a child's own state changes, only the child's render effect runs, so the child's `onUpdated` fires and the parent's does not. A parent re-render also does not force its children to re-render; Vue patches a child only when its props or slot content changed. So `onUpdated` is a render-frequency signal. To react to specific data, watch that source instead.
go deeper
Recall that onUpdated fires whenever the component re-renders, and that each component re-renders on its own.
Explain tracked reads during render, per-component update jobs and why a parent skips an unchanged child.
Diagnose unexpected hook calls with onRenderTriggered or Devtools, and replace onUpdated used as a data listener with a watcher on the specific source.
Use this to set component boundaries: isolating fast-changing state in small children limits re-render scope and keeps render-coupled code predictable.
## The model: one render effect per component Vue 3 compiles each template into a render function and runs it inside a **render effect**. While the render runs, Vue records every reactive read: refs, reactive object properties, props, computed values, injected reactive state. When any recorded dependency changes, Vue queues **that component's** update job. After the patch, it calls that component's `onUpdated`. Two consequences explain the symptoms. ## Why it fires on "unrelated" changes `onUpdated` has no idea which dependency changed. Every tracked read counts equally, so any of these re-renders the component and fires the hook: - a `v-model` draft in the same template (every keystroke) - a `now` ref updated by an interval for relative timestamps - a loading flag or a hover state - a prop whose value the template shows - a store or injected value read in the template Several changes in the same tick collapse into one re-render, so the hook also does not fire once per change. If the log is meant to record "the order data changed", it is measuring the wrong thing. Some things do **not** fire it: a value used only in `<script setup>` logic or in a watcher, never in the template; a write of an identical value, because Vue skips triggers when the value has not changed. ## Why it stays silent when a child changes Updates are **per component**. When a child's own ref changes, the child's render effect runs, the child's DOM is patched, and the **child's** `onUpdated` fires. The parent's render read nothing that changed, so it is not queued and its `onUpdated` does not run. The reverse also holds, which surprises people coming from other frameworks: when the parent re-renders, Vue compares the child's new props (and whether its slot content is dynamic) with the previous ones and **re-renders the child only if something relevant changed**. A parent's re-render therefore does not cascade into `onUpdated` calls for every descendant. One subtle case: reactive state read inside **slot content** is tracked by the component that renders the slot, because the slot function runs during the child's render. | Change | Parent `onUpdated` | Child `onUpdated` | |---|---|---| | Parent state read in parent template | yes | only if the child's props or dynamic slots changed | | Child's own state | no | yes | | Parent state passed as a changed prop | yes, after the child's | yes, before the parent's | | State read only in script, not the template | no | no | For comparison, a framework whose default is that a parent's re-render re-renders its children, such as React without memoization, produces different expectations; in Vue that cascade does not happen by default. ## A worked example An `OrdersPanel` shows a list of orders, a search box bound with `v-model`, and a "last refreshed 12 s ago" label driven by a `now` ref that ticks every second. A developer adds `onUpdated(() => log('orders rendered'))` and sees: - a log line **every second**, from the ticking `now` ref; - a log line **on every keystroke**, from the search draft; - **no** log line when an `OrderRow` child toggles its own expanded state. None of this is a bug. The panel's render reads `now` and the draft, so both re-render it; the row's `expanded` ref is read only by the row. Moving the timestamp into a tiny `RefreshedLabel` child and replacing the hook with `watch(() => orders.value, log)` makes the log mean what it says. ## Diagnosing it in practice 1. **Find out what the render reads.** In development, the `onRenderTriggered` debug hook and Vue Devtools show which dependency caused a re-render. 2. **Decide what you actually want to observe.** If it is a piece of data, use `watch` on that source; if it is DOM after a specific change, use `nextTick()` or a watcher with `flush: 'post'`. 3. **Split noisy state out.** Moving a ticking clock or a draft input into a small child component confines its re-renders to that child, which also makes the parent's `onUpdated` quieter. ## What a strong answer includes - `onUpdated` equals "this component's render effect re-ran", nothing more specific. - Updates are **per component**, driven by tracked reads, not by the tree position. - Parents do not re-render children unconditionally; children do not re-render parents at all. - Replace `onUpdated` used as a data listener with a targeted watcher.
- How would you make the parent's log fire only when its order data changes, not on every re-render?Replace the `onUpdated` log with `watch` on the specific source, for example `watch(() => props.orders, log)` or a getter for the fields you care about. The watcher fires only when that source changes, regardless of what else re-renders the component. If the log must see the patched DOM, add `flush: 'post'`.
- Does a parent's re-render in Vue 3 always re-render its children?No. When the parent patches a child component, Vue checks whether the child's props changed and whether its slots are dynamic. If nothing relevant changed, the child is skipped and neither its render nor its `onUpdated` runs. Each component re-renders only when its own tracked dependencies or its inputs change.
saying these in an interview costs you the question
- A parent's re-render always re-renders every child
- A child's state change re-renders the parent too
- onUpdated only fires for the state it is meant to watch
- onUpdated fires once for each changed reactive value
- State used only in script logic still triggers onUpdated