Suggest an editImprove this articleRefine the answer for “Computed, methods and watchers in Vue.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Computed, methods and watchers** are Vue's three tools for reactive data. Computed caches derived values and recalculates only when dependencies change. Methods run fresh on every call. Watchers fire callbacks when a specific source changes, built for async side effects like API calls. ```javascript const count = ref(0) const doubled = computed(() => count.value * 2) // cached watch(count, (newVal) => fetchData(newVal)) // side effect ``` **Key:** computed for display values, methods for event handlers with params, watchers for API calls and side effects.Shown above the full answer for quick recall.Answer (EN)Image**Computed, methods and watchers** are Vue's three tools for reactive data: computed caches derived values and recalculates only when dependencies change, methods run on every call with no caching, and watchers fire side effects when a specific source mutates. ## Theory ### TL;DR - Computed = cached spreadsheet formula: recalculates only when inputs change - Methods = calculator button: runs fresh on every call, no memory between calls - Watchers = observer: fires a callback when a specific ref or reactive object changes - Need a display value? Computed. Handling a user event with params? Method. Fetching on change? Watcher. - Computed is sync-only; watchers support async (API calls, localStorage, debounce) ### Quick example ```vue <template> <button @click="count++">Clicks: {{ count }}</button> <p>Double (computed): {{ doubleCount }}</p> <p>Double (method): {{ doubleCountMethod() }}</p> </template> <script setup> import { ref, computed, watch } from 'vue' const count = ref(0) const doubleCount = computed(() => { console.log('Computed runs') // Fires once per count change return count.value * 2 }) const doubleCountMethod = () => { console.log('Method runs') // Fires on every render cycle return count.value * 2 } watch(count, (newVal) => { console.log('Watcher fires:', newVal) // Fires on each count change }) </script> ``` Click the button and watch the console: "Computed runs" appears once per click, "Method runs" appears on every render cycle, and the watcher fires each time count changes. ### Key difference Computed properties track reactive dependencies during their first run and cache the result. Vue's reactivity system (Proxy in Vue 3) records which refs were accessed, then invalidates the cache only when those refs change. Methods are plain functions with no tracking whatsoever. Watchers use an explicit source and run callbacks asynchronously through Vue's scheduler, which makes them the right fit for side effects like API calls, DOM updates, or writing to localStorage. ### When to use - Displaying derived data (fullName from firstName + lastName): computed - Handling user events with parameters (form submit, button click): method - Reacting to a data change with an API fetch or localStorage write: watcher - Reading the same value more than once per render: computed, not method - Watching multiple reactive sources in one callback: watcher with an array source - One-off calculation that needs a parameter: method ### Comparison table | Feature | Computed | Methods | Watchers | |---|---|---|---| | **Caching** | Yes, dependency-based | No | No | | **Returns value** | Always | Optional | No | | **Trigger** | Getter access + dep change | Every call | Source change | | **Async support** | No (sync only) | Yes | Yes | | **Side effects** | Not recommended | Fine | Designed for them | | **Best for** | Template display values | Event handlers, parameterized calls | API fetches, debounce, localStorage | ### How Vue handles this internally Vue 3 wraps refs and reactive objects with Proxy. When a computed getter runs for the first time, Vue activates an Effect and records every reactive value accessed via `track()`. On dependency change, the Effect is marked stale and the getter re-runs on next access, not immediately. That is why computed is lazy and cached. Methods bypass this entirely. Watchers use the same Effect mechanism but with an explicit source and flush asynchronously through `queueScheduler`, so they run after Vue finishes the current DOM update cycle. ### Common mistakes **1. Method for repeated display values** ```vue <!-- getFullName() fires on every render --> <template> <p>{{ getFullName() }}</p> <p>{{ getFullName() }}</p> </template> <!-- computed fires once per dependency change --> <template> <p>{{ fullName }}</p> <p>{{ fullName }}</p> </template> <script setup> import { ref, computed } from 'vue' const firstName = ref('Jane') const lastName = ref('Smith') const fullName = computed(() => `${firstName.value} ${lastName.value}`) </script> ``` In a list of 1000 items this difference is measurable. Use computed when the same derived value appears multiple times in the template. **2. Async inside computed** ```vue <script setup> import { ref, computed, watchEffect } from 'vue' const userId = ref(1) const user = ref(null) // Wrong: computed must be synchronous const userName = computed(async () => { user.value = await fetchUser(userId.value) // Returns a Promise, not the name return user.value.name }) // Correct: watchEffect for async watchEffect(async () => { if (userId.value) { user.value = await fetchUser(userId.value) } }) </script> ``` Computed returns a Promise object, not the resolved value. Vue does not await it. This is one of the most common gotchas in Nuxt.js auth modules. **3. Side effects inside computed** ```vue <script setup> // Wrong: side effect fires on every getter access const bad = computed(() => { apiCall() // If template reads this 5 times, apiCall fires 5 times return count.value * 2 }) // Correct watch(count, () => { apiCall() }) </script> ``` **4. Watcher on a non-reactive value** ```vue <script setup> let plainVar = 'hello' // Wrong: plainVar is not reactive, this never triggers watch(plainVar, (newVal) => console.log(newVal)) // Correct: wrap in ref, or pass a getter function const reactiveVar = ref('hello') watch(reactiveVar, (newVal) => console.log(newVal)) </script> ``` **5. Deep watcher on large objects** ```vue // Wrong: recursively scans the entire object on every mutation watch(items, cb, { deep: true }) // Better: watch only what you actually need watch(() => items.value.length, cb) ``` `{ deep: true }` on a 500-item array triggers a full recursive scan on every nested change. Narrow it down where possible. ### Real-world usage - Pinia/Vuex stores: getters are computed properties (filtered todos, cart totals) - Vue Storefront: `total` computed from the cart items array, cached across re-renders - Nuxt.js: watchers on route params to trigger data fetches on navigation - Element Plus: computed for dynamic form validation messages - Vuetify: methods for parameterized button handlers ### Follow-up questions **Q:** Why can't computed handle async operations? **A:** Computed requires a synchronous return value. Returning a Promise gives Vue a Promise object, not the resolved data. Use `watchEffect` or `watch` with an async callback for any async logic. **Q:** How does Vue detect which dependencies a computed property has? **A:** During the getter's first run, Vue activates an Effect and calls `track()` on every reactive Proxy that is accessed. Those become the dependencies. When any of them change, `trigger()` marks the computed as stale. **Q:** What is the difference between `watch` and `watchEffect`? **A:** `watchEffect` runs immediately, auto-collects dependencies from its body, and re-runs when any of them change. `watch` is explicit: you declare the source, it is lazy by default, and the callback receives both old and new values. **Q:** When does a computed property invalidate for object dependencies? **A:** By default Vue tracks only the shallow level: the object reference itself. Mutating a nested property without replacing the ref does not invalidate the computed. Use `reactive` or restructure so the reference itself changes. **Q:** (Senior) If you implement a custom memoized function with a plain closure, what does it miss compared to Vue's computed? **A:** The scheduler. Vue's computed batches invalidations and delays re-runs to the next tick, avoiding redundant recalculations when multiple deps change in the same microtask. A plain closure re-runs immediately on every change with no batching. ## Examples ### Cart total with computed ```vue <script setup> import { ref, computed } from 'vue' const items = ref([ { name: 'Shirt', price: 30, qty: 2 }, { name: 'Pants', price: 60, qty: 1 } ]) const total = computed(() => { // Recalculates only when items array or its contents change return items.value.reduce((sum, item) => sum + item.price * item.qty, 0) }) </script> <template> <ul> <li v-for="item in items" :key="item.name"> {{ item.name }} x{{ item.qty }} = ${{ item.price * item.qty }} </li> </ul> <p>Total: ${{ total }}</p> </template> ``` `total` reads once per render cycle no matter how many times the template references it. Edit any qty and Vue recomputes, but only then. This pattern appears in Vue Storefront and similar e-commerce PWAs. ### Search with debounced watcher ```vue <script setup> import { ref, watch } from 'vue' const query = ref('') const results = ref([]) let timer = null watch(query, (newVal) => { clearTimeout(timer) timer = setTimeout(async () => { if (newVal.trim()) { results.value = await searchAPI(newVal) } else { results.value = [] } }, 300) }) async function searchAPI(q) { const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`) return res.json() } </script> <template> <input v-model="query" placeholder="Type to search..." /> <ul> <li v-for="r in results" :key="r.id">{{ r.title }}</li> </ul> </template> ``` This is the pattern watchers are built for: an async side effect that should debounce, not block the render, and not return a value to the template. Computed would be the wrong tool here. ### Writable computed for two-way binding ```vue <script setup> import { ref, computed } from 'vue' const firstName = ref('Jane') const lastName = ref('Smith') const fullName = computed({ get() { return `${firstName.value} ${lastName.value}` }, set(newValue) { const parts = newValue.split(' ') firstName.value = parts[0] ?? '' lastName.value = parts[1] ?? '' } }) </script> <template> <input v-model="fullName" /> <p>First: {{ firstName }}, Last: {{ lastName }}</p> </template> ``` Writable computed is less common but useful when a parent component works with a combined value while the underlying state stays split. I've seen this in form libraries that expose a single `v-model` binding but store address components separately.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.