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')
})| Hook | When it runs | Typical use |
|---|---|---|
onBeforeMount | Just before the first render | Rarely needed; state is already set up |
onMounted | After the element is in the DOM | Measure, initialise a chart, start a fetch, read localStorage |
onUpdated | After a re-render | Read the DOM after data changed; never mutate state here |
onBeforeUnmount | Before the instance is torn down | Snapshot scroll position, close a connection |
onUnmounted | After teardown | Remove listeners, clear intervals, abort requests |
onActivated / onDeactivated | When a kept-alive component is shown or hidden | Refresh data on return, pause a poll |
- Hooks only register during the synchronous run of
setup. CallingonMountedinside an awaited callback registers nothing and Vue warns about the lost instance. onMountedfires once per instance, not once per visit: withKeepAlivethe component stays mounted, so useonActivatedfor work that must repeat.- Child
onMountedruns before the parent's; teardown goes the other way, parent before child. - Never load required data only in
onMountedif 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: truegives 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: trueis required to see a mutation inside an object, and the cost is that the comparison walks the whole structure on every change.flushcontrols 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 usesonScopeDispose. - 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
awaitor 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.Related
Composables and reusable logic Forms and user input
Last refreshed 2026-09-18.