Reactivity Internals — Series 2

Preview — 3 of 10 questions

What does the set transform let this component do that a bare defineModel() couldn't?

javascript
<script setup>
const model = defineModel({
  get(value) {
    return value?.toUpperCase() ?? ''
  },
  set(value) {
    return value.trim()
  },
})
</script>

<template>
  <input :value="model" @input="model = $event.target.value" />
</template>
Aget/set transforms are invalid on defineModel() — only a plain defineModel() or defineModel('name') (with a name argument) are supported signatures
BdefineModel({ get, set }) lets the component transform the value in both directions independently of the parent's actual bound data — get runs on the incoming prop value before it's exposed locally (here, upper-casing it for display), and set runs on any locally-assigned value before it's emitted back to the parent (here, trimming it) — the parent's own ref stays untouched by the display transform, only receiving the trimmed version
Cget/set transforms replace the underlying modelValue prop and update:modelValue event entirely with different names
DThe set transform only runs once, the first time the component mounts — subsequent local assignments bypass it

Vues reactivity system already knows *exactly* which reactive property changed (via `trigger()` with a specific target/key) — so why does Vue still re-render an entire components VDOM tree and diff it, instead of surgically patching just the one DOM node that depends on that property?

javascript
ref changes  reactivity triggers the component's render effect
            → render() re-runs, producing a NEW VDOM tree
            → Vue diffs new tree vs. old tree
            → only the DOM nodes that actually differ get patched
AVue actually doesn't re-render the whole component — it patches only the single DOM node tied to the changed property directly, with no diffing involved at all
BDiffing is a historical leftover with no remaining purpose in Vue 3 — a future version is expected to remove it entirely
CVue's reactivity system precisely tracks which reactive dependency changed, but a component's render function is the unit of re-execution — re-running it produces a new VDOM tree reflecting whatever that dependency change should visually affect, and diffing against the previous tree is how Vue determines the minimal actual DOM mutations needed, since the component's own JS/template logic (conditionals, loops, computed values) can make the relationship between "one ref changed" and "which DOM nodes are affected" arbitrarily complex — going fully surgical would require tracking effects at the level of individual DOM nodes, which is closer to what "signal"-based frameworks (Solid, for example) do differently, trading Vue's more familiar component-render-function model for something more fine-grained
DDiffing exists purely as a safety net for browser extensions that mutate the DOM outside of Vue's control — it has nothing to do with reactivity

Why doesn't the template re-render after chart.value.data.push(newPoint) here, and what does forceUpdate (via getCurrentInstance()) actually do about it?

javascript
<script setup>
import { ref, markRaw, getCurrentInstance } from 'vue'
import { ChartLibrary } from 'heavy-chart-lib'

const chart = ref(markRaw(new ChartLibrary()))
const instance = getCurrentInstance()

function addPoint(newPoint) {
  chart.value.data.push(newPoint) // mutating a markRaw'd object — invisible to reactivity
  instance.proxy.$forceUpdate()    // ?
}
</script>

<template>
  <p>{{ chart.value.pointCount }}</p>
</template>
A$forceUpdate() makes chart.value.data reactive retroactively, so future mutations are tracked normally from then on
B$forceUpdate() schedules the component's render function to re-run, regardless of whether Vue's reactivity system detected any tracked dependency change — it's an escape hatch for exactly this situation: state changed in a way reactivity couldn't see (here, mutating a markRaw()'d object), but the template still needs to reflect it
C$forceUpdate() only works in the Options API — calling it via getCurrentInstance().proxy in <script setup> silently does nothing
D$forceUpdate() forces a full page reload to guarantee the DOM reflects the latest state

Sign up free to play

Answer all 10 questions (7 more), see explanations for every answer, and track your score.