Lifecycle Hooks
What onMounted, onUpdated and onUnmounted each guarantee, when they run, and why a hook must be registered synchronously.
onMounted(callback: () => void): voidA lifecycle hook is a function you call while a component is being set up, to register a callback for one moment in that component's life: before its first render, after its DOM exists, after an update, and when it is removed from the page.
The ones you need
onMounted runs once, after the component's own DOM tree has been created and inserted into its parent container, and after its synchronous child components have mounted. This is the first moment a real element exists, so measuring, focusing, starting a timer or handing a node to a non-Vue library all belong here. onUnmounted runs after the component has been removed, its children are gone and its watchers have stopped; it undoes whatever onMounted started. Those two cover most of the work you will ever do.
onUpdated runs after any DOM update of the component. Several state changes batch into one update, so it cannot tell you what changed. onBeforeMount, onBeforeUpdate and onBeforeUnmount run immediately ahead of each of those three, and onErrorCaptured receives errors thrown by descendant components.
What trips people up
Hooks register against whichever component is active at that instant, so the call must happen synchronously during setup. Inside a setTimeout, or after an await, it registers nothing and warns at most. Mutating state in onUpdated schedules another update, which calls onUpdated again. Mount, update and unmount hooks are not called during server-side rendering, which makes onMounted the place for client-only code.
Example
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
const seconds = ref(0)
let timer: number | undefined
onMounted(() => {
timer = window.setInterval(() => {
seconds.value += 1
}, 1000)
})
onUnmounted(() => {
window.clearInterval(timer)
})
</script>
<template>
<p class="elapsed">
On screen for <strong>{{ seconds }}</strong> seconds
</p>
</template>
<style scoped>
.elapsed {
font-variant-numeric: tabular-nums;
}
</style>When to use it
- Start an interval when a dashboard panel appears and clear it when the panel is replaced.
- Hand a container element to a charting or map library once the DOM exists.
- Read a value from localStorage on the client only, in a server-rendered app.
- Catch a failing child component and render a fallback instead of an empty screen.
More examples
The order the hooks run in
Mount this and the console shows beforeMount then mounted. Each click adds beforeUpdate then updated. Removing the component adds beforeUnmount then unmounted. Two useful details are visible here: in beforeMount no element exists yet, and a child component's mounted fires before its parent's, because a parent is only mounted once its children are. Registering hooks like this once, in a throwaway component, is the fastest way to stop guessing about ordering.
<script setup lang="ts">
import {
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onMounted,
onUnmounted,
onUpdated,
ref
} from 'vue'
const count = ref(0)
onBeforeMount(() => console.log('beforeMount'))
onMounted(() => console.log('mounted'))
onBeforeUpdate(() => console.log('beforeUpdate'))
onUpdated(() => console.log('updated'))
onBeforeUnmount(() => console.log('beforeUnmount'))
onUnmounted(() => console.log('unmounted'))
</script>
<template>
<button type="button" @click="count += 1">Clicked {{ count }} times</button>
</template>
<style scoped>
button {
padding: 0.4rem 0.8rem;
}
</style>onErrorCaptured as a boundary
The callback receives three arguments: the error, the component instance that threw, and a string describing which part of Vue was running, such as "render function". Returning false stops the error propagating to parent boundaries and the app-level handler. Reach for this when one widget failing should not blank the whole screen. Keep the component thin: the boundary renders the fallback, the child does the work.
<script setup lang="ts">
import { onErrorCaptured, ref } from 'vue'
import ReportTable from './ReportTable.vue'
const failure = ref<string | null>(null)
onErrorCaptured((err: unknown, _instance, info: string) => {
failure.value = err instanceof Error ? err.message : String(err)
console.warn(`error came from ${info}`)
return false
})
</script>
<template>
<p v-if="failure" role="alert">This report could not be shown: {{ failure }}</p>
<report-table v-else />
</template>
<style scoped>
p[role='alert'] {
color: #b00020;
}
</style>Saving on the way out
A pair of hooks reading and writing the same key. onMounted is used rather than setup because localStorage is a browser API and does not exist during server-side rendering. onBeforeUnmount runs while the component is still fully intact, which is what you want if the effect needs to read the DOM; onUnmounted would also work here, since reading a ref does not need an element. Note that neither hook runs if the browser tab is closed outright.
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
const props = defineProps<{ draftKey: string }>()
const text = ref('')
onMounted(() => {
text.value = localStorage.getItem(props.draftKey) ?? ''
})
onBeforeUnmount(() => {
localStorage.setItem(props.draftKey, text.value)
})
</script>
<template>
<label class="field">
Notes
<textarea v-model="text" rows="4"></textarea>
</label>
</template>
<style scoped>
.field {
display: grid;
gap: 0.25rem;
}
</style>
Discussion