跪拜 Guibai
← All articles
Frontend

Destructuring Props in Vue 3 <script setup> Silently Kills Reactivity

By 前端阿凡 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

This bug is easy to ship to production because the code looks idiomatic and works on initial render. The failure is silent and only surfaces when parent data updates, making it a common source of stale-UI regressions in Vue 3 codebases.

Summary

A common pattern in Vue 3 `<script setup>` — destructuring props for cleaner code — causes a hard-to-spot reactivity failure. When a parent component updates a nested field like `user.name`, the child component continues rendering the old value because destructuring grabs a snapshot of the raw data, bypassing Vue's Proxy-based dependency tracking. The issue is especially deceptive with reference types: the destructured variable looks like the same object, but it is disconnected from the reactive proxy.

The root cause is that `defineProps` returns a reactive proxy, and destructuring executes a value copy at compile time. For primitives, the value is copied; for objects, the reference is copied, but that reference points to the un-proxied object from the moment of destructuring. Vue's dependency system only tracks access through `props.xxx`, not through local variables.

Three reliable fixes exist: access props directly in the template (Vue's compiler resolves bare variable names to `props.xxx` automatically), wrap the prop in `computed()` for logic-layer reactivity, or use `toRefs(props)` to convert each prop into an individual reactive Ref. The golden rule is simple — never destructure `defineProps`.

Takeaways
Destructuring `defineProps` in `<script setup>` breaks reactivity because it copies values or references at compile time, bypassing Vue's Proxy layer.
Even reference types like objects lose reactivity when destructured — the local variable holds a snapshot of the un-proxied object.
Vue's template compiler automatically resolves bare variable names to `props.xxx`, so writing `props.` explicitly in templates is unnecessary.
`toRefs(props)` converts each prop into an individual Ref, preserving per-field reactivity after destructuring.
`computed(() => props.user)` keeps a reactive reference that follows parent updates, suitable for logic-layer access.
Deep destructuring like `const { name } = props.user` causes a double break — losing both the prop proxy and the nested object proxy.
Conclusions

The bug is especially dangerous because it passes code review easily: destructuring props is a natural JavaScript idiom and the component renders correctly on mount, hiding the reactivity break until a parent update occurs.

Vue's template auto-resolution of props (where `{{ user.name }}` works without `props.`) creates a false sense that destructured variables are equally reactive, when in fact the template and `<script>` follow entirely different resolution paths.

The TypeScript-only `defineProps` syntax offers no runtime protection against this pattern — type safety does not imply reactivity safety.

Concepts & terms
Reactive Proxy
In Vue 3, `defineProps` returns an object wrapped in a JavaScript Proxy that intercepts property access and mutation, enabling Vue's dependency tracking system to know which components need re-rendering when data changes.
toRefs
A Vue 3 utility that converts a reactive object into a plain object where each property is a standalone Ref, preserving reactivity on individual fields even after destructuring.
From the discussion

The discussion reinforces the article's core warning with a detailed first-hand account of repeatedly encountering the same reactivity pitfall across team members. A key clarification surfaces around reference types: even though destructuring an object or array preserves the reference, it still bypasses the proxy's get trap, making dependency collection incomplete and deep changes untrackable. The practical workaround of using toRefs is endorsed, with a note that its returned refs require .value in JS but auto-unwrap in templates. A brief, low-effort reply of thanks and a single-line code snippet that ignores the problem add little substance.

Destructuring defineProps copies values at that moment, severing the reactive proxy chain so child components never see parent updates.
Reference types are not immune; destructuring them still skips the proxy's get trap, leading to incomplete dependency collection and silently missed deep changes.
Three reliable patterns exist: access props.xxx directly in templates, wrap with computed for JS logic, or use toRefs for safe destructuring.
toRefs returns refs that require .value in JavaScript but auto-unwrap in templates, a common early stumbling block.
Featured comments
向新出发叭 1 likes

I've stepped in this pitfall too, and more than once — almost every time a new colleague joins, it happens all over again. You've nailed the core reason: defineProps returns a reactive proxy object, and destructuring assignment is essentially a value copy, which directly breaks the Proxy's interception chain. The most intuitive way to understand it is: what you get from destructuring is a 'snapshot of that moment,' and when the parent component updates the props later, the child component simply can't perceive it. To add a scenario that's easily confused in real projects: some people think only primitive types (string, number) lose reactivity when destructured, while reference types (object, array) don't. Actually, even though destructuring a reference type gives you the same reference, Vue's dependency collection is completed inside the proxy's get trap. Destructuring bypasses the proxy, so deep property changes on reference types can also fail to be tracked. This issue is especially stealthy during debugging because sometimes it looks 'usable,' but the dependency collection is actually incomplete. My current habit aligns with what you suggested, basically three paths: use props.xxx directly in the template; wrap it with computed when you need it in JS logic; if you really want to destructure, use toRefs — it's one extra line of code but gives peace of mind. Also, a small detail about toRefs: every property it returns is a ref, so you need to access it with .value in JS, but it auto-unwraps in the template. When I first started using it, I often forgot .value and got undefined, but I got used to it later. Great article, explains the problem thoroughly — beginners should avoid a lot of detours after reading it.

前端阿凡

[thanks][thanks][thanks][thanks]

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗