Errors and fixes

Vue 3: props do not update, destructuring and reactivity

Destructuring props in `<script setup>` can break reactivity before Vue 3.5. The fix is `props.x`, `toRefs`, or `defineModel` when you need a `v-model`.

Key points

  • Before Vue 3.5, `const { title } = defineProps()` lost reactivity: the value stayed at its first state.
  • From Vue 3.5 destructuring is reactive, but check the version the project actually resolves.
  • For a `v-model` use `defineModel()`: it removes the hand-written prop plus emit pair.

Short answer: if the value stays at its first state, destructuring copied the prop into a local variable. Use props.title, or toRefs(props). On Vue 3.5 and later destructuring is reactive, but only if that is the version you actually run.

defineProps returns a reactive proxy. The template does not read the prop, it reads the proxy, and the render effect records that read. Destructuring means reading a property once and keeping its value in a plain constant.

Before 3.5 the compiler generated exactly that constant, so no read stayed tracked. From 3.5 the compiler rewrites the accesses so they still go through the proxy, and reactivity survives. The difference is not philosophical: it depends on the version your bundler resolved.


const { title } = defineProps();



  {{ title }} 

The template reads a local constant, not the prop. When the parent changes title, the constant never moves. The same holds for a watch aimed at the copied variable: it watches nothing, because there is no source left to follow.

The three fixes

// 1. accesso diretto: sempre reattivo
const props = defineProps();
// template: {{ props.title }}

// 2. toRefs: ref collegate alla prop
import { toRefs } from "vue";
const { title } = toRefs(props);

// template: {{ title }} (ref scompattata automaticamente)

Vue 3.5 and later compile destructuring and keep it reactive. The point is to check the version: npm ls vue shows the resolved one, which can be older than the version written in package.json, for instance when an icon package pins a different major.

The v-model case


const model = defineModel({ required: true });



  

defineModel removes the hand-written prop plus emit pair. It shipped in Vue 3.4; before that you need explicit binding, meaning :modelValue plus @update:modelValue. It is not sugar: it is the line where the parent-child contract becomes readable.

Diagnosis: resolved version and breaking point

Two questions, in this order. Which version actually runs? And is reactivity lost on the value or on the reference? The first command answers the first question, the second confirms it on the installed package rather than on the lockfile.

npm ls vue
node -p "require('vue/package.json').version"

If the version is 3.5 or later and the value stays stale, the copy is not the cause: the parent mutates an object instead of replacing it, or a local data shares the prop name. In that case the template resolves the local name and the prop never appears.

When the usual fix makes it worse

The first shortcut is to copy the prop into a local ref with a watch that keeps it aligned. It works for static text and adds two sources of truth: there is a tick of delay, and while typing the field rewrites the parent value and loses the caret. The original defect returns as a jumping cursor, which is harder to accept than a frozen heading.

The second shortcut is to write into the prop to close the loop, perhaps with v-model="props.title". With objects the mutation reaches the parent by reference and the development warning only partly disappears: the state turns inconsistent and the data flow stops being reconstructable. Emit an event instead, or use defineModel.

One limit to budget for: reactive destructuring is compiled

From 3.5 the reactivity of destructuring lives in the code the compiler generates, so it holds only where the compiler sees defineProps and the variables derived from it. A destructured value passed to an external module, or closed inside a function defined elsewhere, loses the link: outside that file it is a constant again. With withDefaults and complex types inference can also give up, and reactivity stops with no error.

How to prevent it

  • Do not destructure props when you cannot confirm the Vue version.
  • Use defineModel for a v-model, not a hand-built prop and emit pair.
  • Run vue-tsc in CI: prop type errors surface before runtime.
  • Pin the Vue major in package.json and assert the resolved version in CI.

Where to go next

In the catalogue: components, responsive navbar and blocks. The guides: UI libraries for Vue 3 and TypeScript or JavaScript for the UI. The comparison: accessibility checklist.

Frequently asked questions

Why does the value stay at the first one?

Because destructuring copies the value into a local variable. If your Vue version does not make that copy reactive, the template keeps reading the first value it received.

Does toRefs solve it?

Yes: it turns the props into refs and keeps the link to the proxy. It works on every Vue 3 version, including those before 3.5.

Can I change a prop?

No. Vue warns in development and the data flow breaks. Emit an event, or use defineModel for the two-way case.

How do I check the Vue version?

npm ls vue prints the resolved version, which can differ from the one in package.json because of dependencies. Confirm it by reading the installed package.

Does this apply to ref inside an object?

Yes: replacing the whole object loses the link. Update the property, or reassign a new reactive object with reactive.