Lifecycle hooks, watchers and cleanup

Mount and unmount hooks, watch versus watchEffect with flush timing, and the cleanup that keeps timers, listeners and stale requests from leaking.

Mount, update and unmount

import { ref, onMounted, onUpdated, onBeforeUnmount, onUnmounted, useTemplateRef } from 'vue'

const canvas = useTemplateRef('canvas')      // matches ref="canvas" in the template

onMounted(() => {
  console.log('element is in the DOM and template refs are populated')
  startChart(canvas.value)
})

onUpdated(() => {
  console.log('the component re-rendered')
})

onBeforeUnmount(() => {
  console.log('last chance to read the DOM before teardown')
})

onUnmounted(() => {
  console.log('cleanup: listeners, timers, subscriptions')
})
HookWhen it runsTypical use
onBeforeMountJust before the first renderRarely needed; state is already set up
onMountedAfter the element is in the DOMMeasure, initialise a chart, start a fetch, read localStorage
onUpdatedAfter a re-renderRead the DOM after data changed; never mutate state here
onBeforeUnmountBefore the instance is torn downSnapshot scroll position, close a connection
onUnmountedAfter teardownRemove listeners, clear intervals, abort requests
onActivated / onDeactivatedWhen a kept-alive component is shown or hiddenRefresh data on return, pause a poll
  • Hooks only register during the synchronous run of setup. Calling onMounted inside an awaited callback registers nothing and Vue warns about the lost instance.
  • onMounted fires once per instance, not once per visit: with KeepAlive the component stays mounted, so use onActivated for work that must repeat.
  • Child onMounted runs before the parent's; teardown goes the other way, parent before child.
  • Never load required data only in onMounted if the component also renders on the server — setup runs there, mount does not.

watch, watchEffect and flush timing

import { ref, reactive, watch, watchEffect } from 'vue'

const id = ref(1)
const user = ref(null)
const filters = reactive({ q: '', page: 1 })

watch(id, async (value, oldValue) => { user.value = await load(value) })

watch(() => filters.page, loadPage)          // a getter, not filters.page

watch([id, () => filters.q], ([nextId, nextQ]) => {
  // re-runs when either source changes
}, { immediate: true })

watchEffect(() => {
  document.title = user.value ? user.value.name : 'Loading'
})
  • watch is lazy and explicit: pass a ref, a getter or an array of sources. It tells you the old and new value, and immediate: true gives you a first run.
  • watchEffect runs immediately and re-runs for whatever it read. Convenient, but the dependency list is implicit and changes as you edit the body.
  • Watching a property of a reactive object directly (watch(filters.page, ...)) evaluates once and loses the link. Always pass a getter for a nested value.
  • deep: true is required to see a mutation inside an object, and the cost is that the comparison walks the whole structure on every change.
  • flush controls timing: 'pre' is the default and runs before the DOM updates, 'post' runs after so measurements are current, 'sync' fires immediately and is rarely what you want.
⚠️
A watcher that writes to the value it watches re-triggers itself forever. If the new value is a function of the old one, you wanted a computed; reserve watch for effects that leave the component, such as a request, storage or the DOM.

Cleanup and stopping watchers

import { ref, watch, onUnmounted, onScopeDispose } from 'vue'

const query = ref('')
let timer = null
const controller = ref(null)

onUnmounted(() => {
  clearInterval(timer)
  window.removeEventListener('resize', onResize)
  controller.value?.abort()
})

watch(query, (value, _old, onCleanup) => {
  const current = new AbortController()
  controller.value = current
  onCleanup(() => current.abort())      // runs before the next call and on teardown
  search(value, { signal: current.signal })
})

const stop = watch(query, handler)
stop()                                  // manual teardown, once a condition is met
  • Everything you subscribe to must be undone: intervals, event listeners, observers, sockets and store subscriptions. Vue cannot know about a callback you registered yourself.
  • The third argument of a watch callback is the right place to cancel the previous request, because it runs both before the next invocation and at teardown.
  • Watchers created inside a component are stopped automatically on unmount. A watcher created in a plain module is not, so expose a stop() or wrap the logic in a composable that uses onScopeDispose.
  • A stale response is the classic failure: an earlier request finishes last and overwrites newer data. Aborting it is a fix; a request counter that discards old results is the other.
  • Warnings about lifecycle hooks with no active instance mean cleanup is being registered from the wrong place — usually after an await or from an event handler.

FAQ

When should I use watchEffect instead of watch?
Use watchEffect when the effect reads several reactive values and re-running on any of them is what you want, such as syncing a title. Use watch as soon as you care which value changed, need the previous value, or want immediate and deep control.
Why did onMounted not run again when I navigated back?
The component was kept alive by KeepAlive, so the instance was hidden rather than destroyed. Use onActivated for work that should repeat on each visit and onDeactivated to pause it.

Composables and reusable logic Forms and user input

Last refreshed 2026-09-18.