Fix: Vue Computed Property Not Updating — Reactivity Not Triggered
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 brokenOr 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
refobject entirely without updating.value— direct assignment to arefwithout going through.valueloses 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 reactiveFix 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 case | Use |
|---|---|
| Derive data from reactive state | computed |
| Run async operations on change | watch / watchEffect |
| Complex logic with multiple dependencies | computed |
| Trigger side effects (API calls, DOM) | watch |
| Need old and new values | watch |
| Immediate execution on setup | watchEffect 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:
- Install Vue DevTools (browser extension).
- Select the component in the DevTools panel.
- Find the computed property.
- Click it to see tracked dependencies — the reactive properties it reads.
- 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Vue Slot Not Working — Named Slots Not Rendering or Scoped Slot Data Not Accessible
How to fix Vue 3 slot issues — v-slot syntax, named slots, scoped slots passing data, default slot content, fallback content, and dynamic slot names.
Fix: Pinia State Not Reactive — Store Changes Not Updating the Component
How to fix Pinia store state not updating components — storeToRefs for destructuring, $patch for partial updates, avoiding reactive() wrapping, getters vs computed, and SSR hydration.
Fix: Vue v-model Not Working on Custom Components — Prop Not Syncing
How to fix Vue v-model on custom components — defineModel, modelValue/update:modelValue pattern, multiple v-model bindings, v-model modifiers, and Vue 2 vs Vue 3 differences.
Fix: Vue Router Navigation Guard Not Working — beforeEach and Route Guards
How to fix Vue Router navigation guards not working — beforeEach, beforeEnter, in-component guards, async guards, redirect loops, and route meta authentication patterns.