Skip to content

Fix: Vue Computed Property Not Updating — Reactivity Not Triggered

FixDevs ·

Quick Answer

How to fix Vue computed properties not updating — reactive dependency tracking, accessing nested objects, computed setters, watchEffect vs computed, and Vue 3 reactivity pitfalls.

The Problem

A Vue computed property doesn’t update when its dependencies change:

<script setup>
import { ref, computed } from 'vue';

const user = ref({ name: 'Alice', address: { city: 'Tokyo' } });

const displayName = computed(() => user.value.name);

// Later in code:
user.value = { ...user.value, name: 'Bob' };  // ← Why isn't displayName updating?
</script>

Or a computed property based on a reactive object doesn’t react to property changes:

const state = reactive({ items: ['a', 'b', 'c'] });

const itemCount = computed(() => state.items.length);

state.items.push('d');  // itemCount should update to 4
// But if items was replaced instead: state.items = [...state.items, 'd']
// itemCount may not update depending on how reactivity is broken

Or in Vue 2, adding a new property to an object doesn’t trigger computed updates:

// Vue 2
export default {
  data() {
    return { user: { name: 'Alice' } };
  },
  computed: {
    greeting() {
      return `Hello, ${this.user.nickname}`;  // nickname added later — not reactive
    },
  },
  mounted() {
    this.user.nickname = 'Ali';  // NOT reactive in Vue 2 — computed doesn't update
  },
};

Why This Happens

Vue’s reactivity system tracks dependencies by recording which reactive properties are accessed during a computed property’s execution. The computed value is recalculated only when those tracked dependencies change.

Reactivity breaks when:

  • Replacing a ref object entirely without updating .value — direct assignment to a ref without going through .value loses reactivity.
  • Adding new properties to a reactive() object in Vue 2 — Vue 2’s reactivity can’t detect property additions. Vue 3 (using Proxy) doesn’t have this limitation.
  • Destructuring reactive objects — destructuring a reactive() object breaks reactivity because you get primitive copies, not reactive references.
  • Accessing computed outside the reactive tree — reading computed values in non-reactive contexts (event handlers, lifecycle hooks that run after component unmount) doesn’t track dependencies.
  • Mutating an array with non-reactive methods in Vue 2 — in Vue 2, only specific array mutation methods (push, pop, splice, etc.) are reactive. Direct index assignment (arr[0] = value) is not.
  • Lazy computed with stale closure — computed properties memoize results. If the dependency tracking misses a reactive source (because it’s not accessed during the first evaluation), subsequent changes don’t trigger updates.

Fix 1: Always Access and Modify ref Values Through .value

<script setup>
import { ref, computed } from 'vue';

const user = ref({ name: 'Alice' });
const displayName = computed(() => user.value.name);

// WRONG — replacing the ref object entirely (loses reactivity tracking)
// user = { name: 'Bob' };   ← Can't reassign const ref anyway

// WRONG in Vue 3 — doesn't trigger computed update
// Object.assign(user, { name: 'Bob' });  ← 'user' is the ref wrapper, not the value

// CORRECT — modify through .value
user.value = { ...user.value, name: 'Bob' };  // Replace entire object
// OR
user.value.name = 'Bob';  // Mutate nested property (Vue 3 tracks this)

// Verify with a watcher
watch(displayName, (newVal) => {
  console.log('displayName changed to:', newVal);
});
</script>

Deep reactivity with ref in Vue 3:

<script setup>
import { ref, computed } from 'vue';

const state = ref({
  user: {
    profile: {
      name: 'Alice',
      city: 'Tokyo',
    },
  },
});

// Vue 3's ref() makes nested objects deeply reactive
const cityName = computed(() => state.value.user.profile.city);

// This triggers cityName to update — Vue 3 tracks deep property access
state.value.user.profile.city = 'Osaka';
</script>

Fix 2: Don’t Destructure reactive() Objects

Destructuring a reactive() object extracts primitive values — these are copies, not reactive:

<script setup>
import { reactive, computed, toRef, toRefs } from 'vue';

const state = reactive({ count: 0, name: 'Alice' });

// WRONG — destructuring breaks reactivity
const { count, name } = state;
const doubled = computed(() => count * 2);  // count is a plain number — not reactive
// doubled never updates even when state.count changes

// CORRECT option 1 — access state directly in computed
const doubled = computed(() => state.count * 2);  // state.count is reactive

// CORRECT option 2 — use toRefs() to convert to reactive refs
const { count, name } = toRefs(state);
// count is now a Ref<number> — reactive
const doubled = computed(() => count.value * 2);  // Updates when state.count changes

// CORRECT option 3 — use toRef() for a single property
const countRef = toRef(state, 'count');
const doubled = computed(() => countRef.value * 2);
</script>

In composables — always return toRefs() to preserve reactivity for destructuring callers:

// composables/useUser.ts
export function useUser() {
  const state = reactive({
    name: '',
    email: '',
    isLoading: false,
  });

  // WRONG — caller loses reactivity when destructuring
  // return state;

  // CORRECT — return reactive refs so caller can destructure
  return {
    ...toRefs(state),
    // methods don't need toRef
    async fetchUser(id: number) {
      state.isLoading = true;
      const user = await api.getUser(id);
      state.name = user.name;
      state.email = user.email;
      state.isLoading = false;
    },
  };
}

// Caller can safely destructure
const { name, email, isLoading, fetchUser } = useUser();
// name, email, isLoading are Refs — remain reactive

Fix 3: Fix Array Reactivity Issues

In Vue 3, arrays inside reactive() and ref() are deeply reactive — most mutations trigger updates. But there are still pitfalls:

<script setup>
import { reactive, computed } from 'vue';

const state = reactive({ items: ['a', 'b', 'c'] });
const itemCount = computed(() => state.items.length);

// All of these trigger computed updates in Vue 3:
state.items.push('d');          // ✓ Mutation — reactive
state.items.splice(1, 1);       // ✓ Mutation — reactive
state.items = [...state.items, 'e'];  // ✓ Replace — reactive (state is reactive obj)

// WRONG — replacing with a non-reactive array
// const items = state.items;
// items.push('f');  // Mutating a captured reference — may not trigger update
</script>

When filtering/mapping, return a new array to the reactive state:

<script setup>
const state = reactive({
  todos: [
    { id: 1, text: 'Buy milk', done: false },
    { id: 2, text: 'Write code', done: true },
  ],
});

const incompleteTodos = computed(() =>
  state.todos.filter(todo => !todo.done)
);

// Update a todo's done status — triggers computed update
function toggleTodo(id: number) {
  const todo = state.todos.find(t => t.id === id);
  if (todo) {
    todo.done = !todo.done;  // Reactive — Vue 3 tracks nested mutations
  }
}
</script>

Vue 2 array reactivity limitations:

// Vue 2 — only these array methods are reactive:
// push, pop, shift, unshift, splice, sort, reverse

// WRONG in Vue 2 — direct index assignment NOT reactive
this.items[0] = 'new value';  // Doesn't trigger update

// CORRECT in Vue 2
this.$set(this.items, 0, 'new value');
// or
this.items.splice(0, 1, 'new value');

Fix 4: Use computed with Getters and Setters

Computed properties are read-only by default. To make them writable, provide a setter:

<script setup>
import { ref, computed } from 'vue';

const firstName = ref('Alice');
const lastName = ref('Smith');

// Read-only computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`);

// WRONG — can't assign to a read-only computed
// fullName.value = 'Bob Jones';  // TypeError: computed value is readonly

// Read-write computed with getter and setter
const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`;
  },
  set(newValue: string) {
    [firstName.value, lastName.value] = newValue.split(' ');
  },
});

// Now you can both read and write
console.log(fullName.value);         // 'Alice Smith'
fullName.value = 'Bob Jones';        // Triggers the setter
console.log(firstName.value);        // 'Bob'
console.log(lastName.value);         // 'Jones'
</script>

Computed setter for v-model on computed values:

<template>
  <!-- Works because fullName has a setter -->
  <input v-model="fullName" />
</template>

<script setup>
const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (val) => {
    const [first, ...rest] = val.split(' ');
    firstName.value = first;
    lastName.value = rest.join(' ');
  },
});
</script>

Fix 5: Know When to Use watch vs computed

Computed properties are for derived data. watch and watchEffect are for side effects:

<script setup>
import { ref, computed, watch, watchEffect } from 'vue';

const searchQuery = ref('');
const results = ref([]);

// WRONG — computed for async work (can't use await in computed)
const searchResults = computed(async () => {
  const data = await fetch(`/api/search?q=${searchQuery.value}`);
  return data.json();
  // This returns a Promise — not the actual data
});

// CORRECT — use watch for async side effects
watch(searchQuery, async (query) => {
  if (!query) {
    results.value = [];
    return;
  }
  const data = await fetch(`/api/search?q=${query}`);
  results.value = await data.json();
});

// OR use watchEffect — automatically tracks dependencies
watchEffect(async () => {
  const query = searchQuery.value;  // Tracked dependency
  if (!query) return;
  const data = await fetch(`/api/search?q=${query}`);
  results.value = await data.json();
});

Choose between computed and watch:

Use caseUse
Derive data from reactive statecomputed
Run async operations on changewatch / watchEffect
Complex logic with multiple dependenciescomputed
Trigger side effects (API calls, DOM)watch
Need old and new valueswatch
Immediate execution on setupwatchEffect or watch with { immediate: true }

Fix 6: Debug Reactivity Issues

Vue DevTools shows the computed property’s dependencies and current value — the fastest way to diagnose why it’s not updating:

  1. Install Vue DevTools (browser extension).
  2. Select the component in the DevTools panel.
  3. Find the computed property.
  4. Click it to see tracked dependencies — the reactive properties it reads.
  5. If a dependency you expect to see is missing, Vue isn’t tracking it.

Manual debugging with watchEffect:

<script setup>
import { watchEffect } from 'vue';

// watchEffect runs immediately and logs all tracked dependencies
watchEffect(() => {
  console.log('Tracked: user.name =', user.value.name);
  console.log('Tracked: settings.theme =', settings.theme);
  // This tells you exactly what Vue is tracking
});
</script>

Check if a value is reactive:

import { isRef, isReactive, isReadonly } from 'vue';

console.log(isRef(myValue));       // true if it's a ref
console.log(isReactive(myValue));  // true if it's reactive
console.log(isReadonly(myValue));  // true if it's readonly (e.g., computed)

Still Not Working?

Computed value returns a Promise — if the getter function is async, the computed property’s value is a Promise, not the resolved data. Use watch for async operations (see Fix 5).

Stale computed after component prop change — if a computed property depends on a prop and the parent changes the prop, the computed should update automatically. If it doesn’t, verify the prop is declared correctly:

<script setup>
// Props are reactive — computed works correctly
const props = defineProps<{ userId: number }>();

const userEndpoint = computed(() => `/api/users/${props.userId}`);
// Updates when userId prop changes ✓
</script>

Pinia store values in computed — accessing Pinia store state in computed works with the Composition API:

<script setup>
import { computed } from 'vue';
import { useUserStore } from '@/stores/user';

const store = useUserStore();

// CORRECT — access store properties directly (they're reactive)
const displayName = computed(() => store.user?.name ?? 'Guest');

// WRONG — destructuring breaks reactivity (same as reactive())
// const { user } = store;  ← user is no longer reactive
</script>

For related Vue issues, see Fix: Vue Reactive Data Not Updating and Fix: Vue Router Navigation Guard Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles