Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3 debugging reference for reactivity issues, computed errors, watcher bugs, async failures, and SSR hydration problems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/watch-async-cleanup.md
1---2title: Clean Up Async Operations in Watchers to Prevent Race Conditions3impact: HIGH4impactDescription: Stale async requests can overwrite newer data, causing incorrect UI state and hard-to-debug issues5type: capability6tags: [vue3, watch, watchers, async, cleanup, race-condition, abort]7---89# Clean Up Async Operations in Watchers to Prevent Race Conditions1011**Impact: HIGH** - When a watched value changes rapidly, multiple async operations run concurrently. Without cleanup, a slow earlier request can complete after a faster later request, overwriting current data with stale results.1213Always use `onWatcherCleanup` or the `onCleanup` callback parameter to cancel pending async operations when the watcher re-runs or the component unmounts.1415## Task Checklist1617- [ ] Use `onWatcherCleanup()` or `onCleanup` parameter in async watchers18- [ ] Use `AbortController` to cancel pending fetch requests19- [ ] Cancel any setTimeout/setInterval calls in cleanup20- [ ] Invalidate previous async operation results with flags21- [ ] Consider debouncing rapid changes before fetching2223**Incorrect:**24```javascript25import { ref, watch } from 'vue'2627const searchQuery = ref('')28const results = ref([])2930// BAD: Race condition - slow request for "a" can finish after fast request for "ab"31watch(searchQuery, async (query) => {32if (query) {33const response = await fetch(`/api/search?q=${query}`)34results.value = await response.json() // May overwrite newer results!35}36})3738// BAD: No cleanup for timeouts39watch(searchQuery, (query) => {40// Previous timeout keeps running even when query changes41setTimeout(() => {42performExpensiveSearch(query)43}, 500)44})45```4647**Correct:**48```javascript49import { ref, watch, onWatcherCleanup } from 'vue'5051const searchQuery = ref('')52const results = ref([])53const loading = ref(false)5455// CORRECT: Using onWatcherCleanup (Vue 3.5+)56watch(searchQuery, async (query) => {57if (!query) {58results.value = []59return60}6162const controller = new AbortController()6364// Register cleanup to abort on re-run or unmount65onWatcherCleanup(() => {66controller.abort()67})6869loading.value = true70try {71const response = await fetch(`/api/search?q=${query}`, {72signal: controller.signal73})74results.value = await response.json()75} catch (err) {76if (err.name !== 'AbortError') {77console.error('Search failed:', err)78}79} finally {80loading.value = false81}82})83```8485## Using onCleanup Parameter8687```javascript88import { ref, watch } from 'vue'8990const userId = ref(1)91const userData = ref(null)9293// CORRECT: Using onCleanup callback parameter94watch(userId, (newId, oldId, onCleanup) => {95const controller = new AbortController()9697fetch(`/api/users/${newId}`, { signal: controller.signal })98.then(res => res.json())99.then(data => {100userData.value = data101})102.catch(err => {103if (err.name !== 'AbortError') {104console.error(err)105}106})107108onCleanup(() => {109controller.abort()110})111})112```113114## Cleanup with Timeouts115116```javascript117import { ref, watch, onWatcherCleanup } from 'vue'118119const input = ref('')120121// CORRECT: Cancel previous timeout on new input122watch(input, (value) => {123const timeoutId = setTimeout(() => {124performExpensiveOperation(value)125}, 300)126127onWatcherCleanup(() => {128clearTimeout(timeoutId)129})130})131```132133## Invalidation Flag Pattern134135```javascript136import { ref, watch } from 'vue'137138const id = ref(1)139const data = ref(null)140141// CORRECT: Invalidation flag for non-abortable operations142watch(id, async (newId, oldId, onCleanup) => {143let cancelled = false144145onCleanup(() => {146cancelled = true147})148149const result = await someNonAbortableAsyncOperation(newId)150151// Check if this watch run is still valid152if (!cancelled) {153data.value = result154}155})156```157158## watchEffect Cleanup159160```javascript161import { ref, watchEffect, onWatcherCleanup } from 'vue'162163const resourceId = ref('abc')164165watchEffect(async () => {166const id = resourceId.value167const controller = new AbortController()168169onWatcherCleanup(() => {170controller.abort()171})172173const data = await fetchResource(id, { signal: controller.signal })174processData(data)175})176```177178## Reference179- [Vue.js Watchers - Callback Flush Timing](https://vuejs.org/guide/essentials/watchers.html#callback-flush-timing)180- [Vue.js Watchers - Side Effect Cleanup](https://vuejs.org/api/reactivity-core.html#watcheffect)181