Vue 3 reactive reassignment trap illustration

The surprising bug

You have a reactive() object and want to “replace” it with a fresh one — reset a form, load new data, swap a user. So you reassign the variable:

import { reactive } from 'vue'

let user = reactive({ name: 'Ada', age: 36 })

function loadNewUser() {
  // 💥 reassigning the variable
  user = reactive({ name: 'Grace', age: 41 })
}

Call loadNewUser() and your template still shows Ada, 36. Nothing updates. Why?

Why it doesn’t work

reactive() returns a proxy bound to the original object. When you write user = reactive({...}), user now points at a different proxy — a brand-new reactive wrapper. But every template, computed, and watcher captured a reference to the original proxy through the component’s setup scope. They never see the new one, so no dependency is ever notified.

In short:

  • reactive() reactivity is per-object, not per-variable.
  • Reassigning the variable swaps the object, but not the bindings already pointing at the old one.

Fix 1 — use ref() for objects (recommended)

ref() wraps the value in a .value getter/setter, so reassigning .value is tracked:

import { ref } from 'vue'

const user = ref({ name: 'Ada', age: 36 })

function loadNewUser() {
  user.value = { name: 'Grace', age: 41 } // ✅ reactive
}

Fix 2 — mutate in place

If you prefer reactive(), keep the same object and overwrite its properties:

import { reactive } from 'vue'

const user = reactive({ name: 'Ada', age: 36 })

function loadNewUser() {
  // ✅ mutate instead of reassign
  Object.assign(user, { name: 'Grace', age: 41 })
}

Rule of thumb

  • Need to replace the whole object/array? Use ref().
  • Need nested reactivity and only ever mutate fields? reactive() is fine.
  • Never reassign a reactive() variable and expect the UI to follow.

A 30-second gotcha that saves an afternoon of “why isn’t my view updating” debugging.