---
title: "Reactive props destructure in Vue 3.5, explained · vuejs.school"
description: "Vue 3.5 lets you destructure defineProps() without losing reactivity. Here is what the compiler rewrites, how defaults work, and where it still bites."
canonical_url: "https://vuejs.school/community/blog/reactive-props-destructure-in-vue-3-5"
meta:
  "og:description": "Vue 3.5 lets you destructure defineProps() without losing reactivity. Here is what the compiler rewrites, how defaults work, and where it still bites."
  "og:image": "https://vuejs.school/_og/d/a_~UmVhY3RpdmUgcHJvcHMgZGVzdHJ1Y3R1cmUgaW4gVnVlIDMuNSwgZXhwbGFpbmVkIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~UmVhY3RpdmUgcHJvcHMgZGVzdHJ1Y3R1cmUgaW4gVnVlIDMuNSwgZXhwbGFpbmVk%2Cdescription_~VnVlIDMuNSBsZXRzIHlvdSBkZXN0cnVjdHVyZSBkZWZpbmVQcm9wcygpIHdpdGhvdXQgbG9zaW5nIHJlYWN0aXZpdHkuIEhlcmUgaXMgd2hhdCB0aGUgY29tcGlsZXIgcmV3cml0ZXMsIGhvdyBkZWZhdWx0cyB3b3JrLCBhbmQgd2hlcmUgaXQgc3RpbGwgYml0ZXMu%2Ceyebrow_~RGVlcCBkaXZlIMK3IDYgbWluIHJlYWQ%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yZWFjdGl2ZS1wcm9wcy1kZXN0cnVjdHVyZS1pbi12dWUtMy01Ig%2Cs_lEDKtTmstg71lFNv.png"
  "og:image:alt": "Reactive props destructure in Vue 3.5, explained · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Reactive props destructure in Vue 3.5, explained · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~UmVhY3RpdmUgcHJvcHMgZGVzdHJ1Y3R1cmUgaW4gVnVlIDMuNSwgZXhwbGFpbmVkIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~UmVhY3RpdmUgcHJvcHMgZGVzdHJ1Y3R1cmUgaW4gVnVlIDMuNSwgZXhwbGFpbmVk%2Cdescription_~VnVlIDMuNSBsZXRzIHlvdSBkZXN0cnVjdHVyZSBkZWZpbmVQcm9wcygpIHdpdGhvdXQgbG9zaW5nIHJlYWN0aXZpdHkuIEhlcmUgaXMgd2hhdCB0aGUgY29tcGlsZXIgcmV3cml0ZXMsIGhvdyBkZWZhdWx0cyB3b3JrLCBhbmQgd2hlcmUgaXQgc3RpbGwgYml0ZXMu%2Ceyebrow_~RGVlcCBkaXZlIMK3IDYgbWluIHJlYWQ%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yZWFjdGl2ZS1wcm9wcy1kZXN0cnVjdHVyZS1pbi12dWUtMy01Ig%2Cs_lEDKtTmstg71lFNv.png"
  "twitter:image:alt": "Reactive props destructure in Vue 3.5, explained · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Why destructuring used to lose reactivity

`defineProps()` returns a reactive object. Vue tracks reads on it by property: when a computed or a template reads `props.count`, Vue notes that it depends on `count`, and re-runs it when the parent passes a new value.

Destructuring copies values out of an object at the moment it runs. In Vue 3.4 and earlier, this:

```
const { count } = defineProps<{ count: number }>()
```

ran once during setup and gave you a plain constant. The template still updated, because the template reads through `props` under the hood, but anything in the script that used `count` saw the first value forever. A `watchEffect` that logged `count` ran once and never again. That silent staleness is why the Vue docs told you to keep `props.` in front of everything.

Vue 3.3 shipped an experimental, opt-in fix. Vue 3.5, released in September 2024, declared it stable and turned it on by default ([the 3.5 announcement](https://blog.vuejs.org/posts/vue-3-5)).

## What the compiler rewrites

The fix lives in the compiler, not at runtime. When `<script setup>` destructures `defineProps()`, the compiler drops the destructure and remembers each name. Wherever one of those names is used in the same script block, it writes a read from the props object instead. In the compiled component that object is called `__props`. So this:

*components/Price.vue*

```
<script setup lang="ts">
import { computed } from 'vue'

const { amount, currency } = defineProps<{ amount: number, currency: string }>()

const label = computed(() => `${amount.toFixed(2)} ${currency}`)
</script>
```

compiles as if you had written this:

```
const __props = defineProps<{ amount: number, currency: string }>()

const label = computed(() => `${__props.amount.toFixed(2)} ${__props.currency}`)
```

The read of `amount` now happens inside the computed, on the reactive object, every time the computed runs. Reactivity is intact because the destructure never really happened. The template is unaffected either way: `{{ amount }}` always read through the props object, which is why templates never needed the `props.` prefix in the first place.

Two consequences follow from this:

- The rewrite only reaches code in the same `<script setup>` block. A function written in another file gets whatever value you pass it, which is the cause of the second gotcha below.
- A destructured prop is still a prop, so you cannot assign to it. `amount = 5` is a compile error: `Cannot assign to destructured props as they are readonly.`

If you want to see which identifiers are props while you read, the Vue extension for VS Code has an inlay-hints setting that marks destructured props in the editor (the [props guide](https://vuejs.org/guide/components/props.html#reactive-props-destructure) mentions it).

## Declare defaults without withDefaults

Before 3.5, a type-based props declaration needed `withDefaults()` to give a prop a default value. With destructure, you use JavaScript's own default syntax:

*components/Pagination.vue*

```
<script setup lang="ts">
const {
  page = 1,
  perPage = 20,
  sizes = [10, 20, 50],
} = defineProps<{ page?: number, perPage?: number, sizes?: number[] }>()
</script>

<template>
  <p>Page {{ page }}, {{ perPage }} per page</p>
  <select><option v-for="s in sizes" :key="s">{{ s }}</option></select>
</template>
```

The compiler turns these into the runtime `default` options you would have written by hand. Look at `sizes`: with `withDefaults` you had to write `sizes: () => [10, 20, 50]`, a factory function, so that each component instance got its own array. The compiler now does that wrapping for you whenever the default is not a plain literal, so an array or object default is safe to write directly.

The compiler also checks the default against the declared type. `page = '1'` with `page?: number` fails the build with `Default value of prop "page" does not match declared type.`

`withDefaults()` still works and is not deprecated. Prefer the destructure form in new code; it is shorter and keeps the default next to the name.

## Watch a destructured prop through a getter

The first gotcha looks like a normal watcher, but the build rejects it:

*components/Search.vue**Breaks***

```
<script setup lang="ts">
import { watch } from 'vue'

const { query } = defineProps<{ query: string }>()

watch(query, (next) => {
  console.log('query changed to', next)
})
</script>
```

Remember the rewrite: `watch(query, ...)` would become `watch(__props.query, ...)`, which passes a string to `watch`. A string is a value, not a source, so there would be nothing to watch. The compiler catches this case and stops the build with:

```
"query" is a destructured prop and should not be passed directly to watch(). Pass a getter () => query instead.
```

Do what it says. A getter is a function that returns the value; `watch` calls it, sees the read of `__props.query` inside, and tracks it:

*components/Search.vue**Works***

```
<script setup lang="ts">
import { watch } from 'vue'

const { query } = defineProps<{ query: string }>()

watch(() => query, (next) => {
  console.log('query changed to', next)
})
</script>
```

This is the same rule as before 3.5, when you wrote `watch(() => props.query, ...)`. `watchEffect` needs no getter, because the read of `query` already happens inside the effect function.

The compiler runs the same check on `toRef(query)`, and the message then names `toRef()`. Those two calls are the only ones it checks, which brings us to the second gotcha.

## Pass a getter to composables

Composables (functions that bundle reactive logic so several components can share it) usually take a ref or a getter so they can react to changes. Pass a destructured prop directly and the compiler cannot help you, because it has no idea what your function expects:

*components/UserCard.vue**Breaks***

```
<script setup lang="ts">
import { useFetchUser } from '@/composables/useFetchUser'

const { userId } = defineProps<{ userId: number }>()

const { user } = useFetchUser(userId)
</script>
```

This compiles without complaint, into `useFetchUser(__props.userId)`. If the parent passes `42`, the composable receives the number 42, fetches user 42, and never notices when the parent switches to user 43. The component shows stale data and nothing warns you.

Wrap it in a getter, exactly as with `watch`:

*components/UserCard.vue**Works***

```
<script setup lang="ts">
import { useFetchUser } from '@/composables/useFetchUser'

const { userId } = defineProps<{ userId: number }>()

const { user } = useFetchUser(() => userId)
</script>
```

On the receiving side, the composable should accept a ref, a getter or a plain value, and normalise with `toValue()` from Vue wherever it reads the input:

*composables/useFetchUser.ts*

```
import { ref, toValue, watchEffect, type MaybeRefOrGetter } from 'vue'

export function useFetchUser(id: MaybeRefOrGetter<number>) {
  const user = ref<{ name: string } | null>(null)

  watchEffect(async () => {
    const response = await fetch(`/api/users/${toValue(id)}`)
    user.value = await response.json()
  })

  return { user }
}
```

`toValue(id)` inside `watchEffect` calls the getter, which reads `__props.userId`, which the effect tracks. If you would rather hand the composable a ref, `toRef(() => userId)` makes one from the getter.

The same care applies to `provide()`. Writing `provide('userId', userId)` hands the children the current number, and the compiler says nothing. Provide a computed, `provide('userId', computed(() => userId))`, and the children can follow the prop as it changes.

## The rule behind both gotchas

Both problems have the same shape. A destructured prop is `props.count` in disguise. Reading it where you need the value, in a `computed`, a `watchEffect` or the template, is fine: the compiler makes that read reactive. Passing it to a function hands over the current value, not the connection to the prop. Whenever a function needs to see future values, give it `() => count`.

## Turn props destructure off for a project

Most projects should leave the feature on: the code reads better, and the two gotchas are easy to spot once you know the rewrite. If your team prefers the explicit `props.count` style, switch it off in the Vue plugin's options:

*vite.config.ts*

```
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue({ features: { propsDestructure: false } })],
})
```

The `features` option needs `@vitejs/plugin-vue` 5.1 or later; it replaces the deprecated `script: { propsDestructure: false }`.

`false` does not stop anyone destructuring. A destructured prop then compiles to a plain constant, with no warning. In 3.5 the template reads that constant too: unlike in 3.4, `{{ amount }}` also keeps showing the first value.

To forbid destructuring outright, the compiler also accepts `'error'`, which fails the build on any props destructure with `Props destructure is explicitly prohibited via config.` The plugin passes that value on, but its 5.2 types allow only `true` or `false`, so TypeScript flags it in `vite.config.ts`.

**Check it stuck**3 questions from the Reactivity in depth quiz.

[**Practice now**](https://vuejs.school/learn/advanced/reactivity-in-depth?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Release notes · 7 min read**Vue 3.6 and Vapor Mode: where things stand**](https://vuejs.school/community/blog/vue-3-6-and-vapor-mode-where-things-stand) [Deep dive · 7 min read**Vapor Mode, explained without the hype**](https://vuejs.school/community/blog/vapor-mode-explained-without-the-hype)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/reactive-props-destructure-in-vue-3-5#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-02-25","description":"Vue 3.5 lets you destructure defineProps() without losing reactivity. Here is what the compiler rewrites, how defaults work, and where it still bites.","name":"Reactive props destructure in Vue 3.5, explained","url":"https://vuejs.school/community/blog/reactive-props-destructure-in-vue-3-5","breadcrumb":{"@id":"https://vuejs.school/community/blog/reactive-props-destructure-in-vue-3-5#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/reactive-props-destructure-in-vue-3-5"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/reactive-props-destructure-in-vue-3-5#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-02-25","description":"Vue 3.5 lets you destructure defineProps() without losing reactivity. Here is what the compiler rewrites, how defaults work, and where it still bites.","headline":"Reactive props destructure in Vue 3.5, explained","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~UmVhY3RpdmUgcHJvcHMgZGVzdHJ1Y3R1cmUgaW4gVnVlIDMuNSwgZXhwbGFpbmVkIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~UmVhY3RpdmUgcHJvcHMgZGVzdHJ1Y3R1cmUgaW4gVnVlIDMuNSwgZXhwbGFpbmVk%2Cdescription_~VnVlIDMuNSBsZXRzIHlvdSBkZXN0cnVjdHVyZSBkZWZpbmVQcm9wcygpIHdpdGhvdXQgbG9zaW5nIHJlYWN0aXZpdHkuIEhlcmUgaXMgd2hhdCB0aGUgY29tcGlsZXIgcmV3cml0ZXMsIGhvdyBkZWZhdWx0cyB3b3JrLCBhbmQgd2hlcmUgaXQgc3RpbGwgYml0ZXMu%2Ceyebrow_~RGVlcCBkaXZlIMK3IDYgbWluIHJlYWQ%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yZWFjdGl2ZS1wcm9wcy1kZXN0cnVjdHVyZS1pbi12dWUtMy01Ig%2Cs_lEDKtTmstg71lFNv.png","@type":["Article","BlogPosting"],"articleSection":["Deep dive"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/reactive-props-destructure-in-vue-3-5#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/reactive-props-destructure-in-vue-3-5#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/reactive-props-destructure-in-vue-3-5#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Reactive props destructure in Vue 3.5, explained","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~UmVhY3RpdmUgcHJvcHMgZGVzdHJ1Y3R1cmUgaW4gVnVlIDMuNSwgZXhwbGFpbmVkIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~UmVhY3RpdmUgcHJvcHMgZGVzdHJ1Y3R1cmUgaW4gVnVlIDMuNSwgZXhwbGFpbmVk%2Cdescription_~VnVlIDMuNSBsZXRzIHlvdSBkZXN0cnVjdHVyZSBkZWZpbmVQcm9wcygpIHdpdGhvdXQgbG9zaW5nIHJlYWN0aXZpdHkuIEhlcmUgaXMgd2hhdCB0aGUgY29tcGlsZXIgcmV3cml0ZXMsIGhvdyBkZWZhdWx0cyB3b3JrLCBhbmQgd2hlcmUgaXQgc3RpbGwgYml0ZXMu%2Ceyebrow_~RGVlcCBkaXZlIMK3IDYgbWluIHJlYWQ%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yZWFjdGl2ZS1wcm9wcy1kZXN0cnVjdHVyZS1pbi12dWUtMy01Ig%2Cs_lEDKtTmstg71lFNv.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~UmVhY3RpdmUgcHJvcHMgZGVzdHJ1Y3R1cmUgaW4gVnVlIDMuNSwgZXhwbGFpbmVkIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~UmVhY3RpdmUgcHJvcHMgZGVzdHJ1Y3R1cmUgaW4gVnVlIDMuNSwgZXhwbGFpbmVk%2Cdescription_~VnVlIDMuNSBsZXRzIHlvdSBkZXN0cnVjdHVyZSBkZWZpbmVQcm9wcygpIHdpdGhvdXQgbG9zaW5nIHJlYWN0aXZpdHkuIEhlcmUgaXMgd2hhdCB0aGUgY29tcGlsZXIgcmV3cml0ZXMsIGhvdyBkZWZhdWx0cyB3b3JrLCBhbmQgd2hlcmUgaXQgc3RpbGwgYml0ZXMu%2Ceyebrow_~RGVlcCBkaXZlIMK3IDYgbWluIHJlYWQ%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yZWFjdGl2ZS1wcm9wcy1kZXN0cnVjdHVyZS1pbi12dWUtMy01Ig%2Cs_lEDKtTmstg71lFNv.png"}]}
```