Destructuring Props in Vue 3 <script setup> Silently Kills Reactivity
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.
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`.
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.
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.
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]