LEARN · DEBUGGING GUIDE

Why Your Vue Composition API ref Isn’t Reactive

Refs in the Vue Composition API often fail to react for surprising reasons: scope, destructuring, or incorrect patterns. Here’s how to diagnose and fix them.

IntermediateVue5 min read

What this usually means

When a ref isn’t reactive in Vue’s Composition API, it almost always points to improper access patterns: either the ref is being destructured (losing its getter/setter), you’re assigning its .value to a plain variable, or you’ve stepped outside the reactivity context (e.g., using refs outside setup). Less often, refs get shadowed, or a stale closure captures an old ref value. Vue’s reactivity system depends on proxies and getters/setters—break that chain, and updates silently stop propagating.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Check if you are accessing ref.value everywhere—in both script and template.
  • 2Search for any destructuring of refs, e.g., `const { count } = myRefObject`.
  • 3Scan for assignments like `let foo = myRef.value;` and subsequent mutations of `foo`.
  • 4Look for refs imported from outside setup(), they lose reactivity outside the context.
  • 5Add a watcher—`watch(myRef, val => console.log(val))`—to see if changes actually propagate.
  • 6Use Vue Devtools: confirm whether your ref’s value actually changes when expected.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchsetup() function in your component script
  • searchPlaces where computed, watch, or watchEffect are used
  • searchAll destructuring statements in the script (look for curly braces and .value)
  • searchComponent props: check if props are being wrapped in ref incorrectly
  • searchVue Devtools ‘Component’ tab: observe ref value live
  • searchVue runtime warnings in the browser console
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningDestructuring ref objects and losing their getter/setter (e.g., `{ count } = useCounter()`)
  • warningAssigning ref.value to a variable and then mutating the variable
  • warningPassing a ref to a child component without using .value in props
  • warningAccessing or mutating the ref outside of setup(), breaking reactivity context
  • warningReturning ref.value instead of the ref object from composables
  • warningMixing ref and reactive—nesting a ref inside a reactive object or vice versa
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildAlways use ref.value in <script>, never destructure refs directly
  • buildPass the whole ref object to composables and child components when shared reactivity is intended
  • buildFor object refs, use toRefs() or reactive() as appropriate, but don’t mix them carelessly
  • buildRefactor destructured assignments to direct ref access: `count.value = ...` not `count = ...`
  • buildIf using withOptions API, ensure refs are not leaking outside Composition API boundaries
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedWatch the UI—template should update immediately when you mutate ref.value
  • verifiedConsole.log the ref’s value (not a shadow variable) after each mutation
  • verifiedCheck Vue Devtools: the Component tab should reflect real-time changes in the ref
  • verifiedAdd a watch() on the ref and verify it fires for every change
  • verifiedUnit test mutations and ensure dependent computed values or effects rerun
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDestructuring refs from objects (using `{ foo } = obj` with refs)
  • warningStoring ref.value in local variables for later mutation
  • warningPassing ref.value instead of the ref object as a prop, losing reactivity
  • warningCombining ref and reactive in the same data structure without using toRefs()
  • warningCreating refs outside of setup() or composables, which defeats reactivity
( 07 )War story

Non-Reactive Counter: A Composition API Mishap

Frontend EngineerVue 3.2, Composition API, Vuex, Vite

Timeline

  1. 09:05Ticket filed: counter UI doesn’t increment despite button press
  2. 09:10Confirmed: `countRef.value` increments in console, but DOM doesn’t update
  3. 09:12Found destructuring: `const { count } = useCounter()`
  4. 09:15Rewrote as `const count = useCounter().count`
  5. 09:16UI still fails—count is a number, not a ref
  6. 09:18Traced useCounter: returns `{ count: ref(0) }`, destructured too early
  7. 09:21Refactored to always use `.value` on count; UI updates as expected

I got called in after a QA flagged our counter component for not updating its displayed value. Oddly, the count increased in the console, but the UI always showed zero.

Initial suspicion fell on the Composition API. The dev who wrote it destructured the result of `useCounter`, grabbing `count` as a primitive instead of a ref. This broke the proxy chain.

After tracing it through, we rewrote the destructuring out and made sure we always used `.value` to access and mutate the ref. Finally, the UI snapped back to full reactivity.

Root cause

Destructuring a ref from a returned object in a composable, losing Vue’s proxy and reactivity.

The fix

Never destructure refs; always access .value on the ref object in script sections.

The lesson

Vue’s reactivity hinges on preserving the ref object. Destructuring kills reactivity—stick with direct .value access everywhere.

( 08 )Destructuring and Ref Loss: How Reactivity Breaks

Vue’s reactivity system relies on proxies and specific accessors (getters/setters) on ref objects. When you destructure a ref—`const { foo } = myRefObject`—you extract the underlying value, not the ref proxy. This immediately cuts Vue out of the update cycle.

Instead, always use `toRefs()` when pulling multiple refs from a reactive object, and avoid curly-brace destructuring with refs altogether. If you really need multiple properties, return refs by name and access them explicitly.

( 09 )How and Why .value is Mandatory

Ref objects are wrappers; the actual value lives inside `ref.value`. In templates, Vue unwraps refs automatically, but in your script, skipping `.value` gives you the ref proxy, not the value.

Assigning `let foo = myRef.value` is fine for reading, but if you set `foo = 5`, the ref won't update. Always mutate via `myRef.value = ...` if you want reactivity.

( 10 )Context: Where Your Ref Lives Matters

Refs created outside `setup()` won’t be reactive in the context of your component, because the reactivity system is only set up for refs inside `setup()` or composable functions called from it.

If you’re importing refs or creating them in module scope, expect reactivity to silently fail unless you’re deeply careful with context and stores.

( 11 )Ref vs. Reactive: Pitfalls of Combining Them

Mixing `reactive` and `ref` can lead to subtle bugs. If you nest a ref inside a reactive object, or vice versa, the reactivity chain can get muddled. Use `toRefs()` to split a reactive object into refs if you need to pass them around.

Keep your data model clean: use `ref` for primitives and `reactive` for objects, and convert between them explicitly when necessary.

Frequently asked questions

Can I destructure refs returned by a composable?

No. Destructuring refs (`const { foo } = useBar()`) extracts the value, not the ref proxy, breaking reactivity. Always access refs by name and use `.value` in scripts.

Why does the template update, but not my JavaScript?

In templates, Vue automatically unwraps refs. In scripts, you must explicitly use `.value` to get or set the underlying value, or reactivity won’t work.

Is it safe to pass ref.value as a prop to child components?

No. Pass the ref object itself if you want two-way reactivity. Passing ref.value copies the value at that instant, losing future updates.

How do I debug if my ref is actually reactive?

Add a `watch()` on the ref, change its value, and check if the watcher fires. Also, inspect the value in Vue Devtools while interacting.

What's the difference between ref and reactive with objects?

`ref` is for single values, `reactive` is for objects. If you need to break up a reactive object into individual refs, use `toRefs()`. Don’t mix these inconsistently.