Lifecycle Hooks (Composition + Options Side by Side)
Same lifecycle, two APIs. Composition API uses imported functions inside setup (onMounted(() => ...)); Options API uses named options (mounted() { ... }). Vue 3 renamed beforeDestroy/destroyed → beforeUnmount/unmounted and added concurrent-aware hooks (onRenderTracked/onRenderTriggered) for debugging reactivity. The Composition hooks must be registered synchronously during setup — same constraint as React hooks.
List the lifecycle hooks and what each is for.
Composition (in setup) |
Options API | Fires |
|---|---|---|
(setup body itself) |
beforeCreate, created |
before the component instance is created (rarely needed in Composition API; the setup body itself runs at this stage) |
onBeforeMount |
beforeMount |
before first render → DOM |
onMounted |
mounted |
after first render; DOM available |
onBeforeUpdate |
beforeUpdate |
after state changed, before DOM update |
onUpdated |
updated |
after DOM update from reactive change |
onBeforeUnmount |
beforeUnmount |
before component is removed |
onUnmounted |
unmounted |
after component is removed (cleanup) |
onErrorCaptured |
errorCaptured |
catches errors from descendants |
onActivated |
activated |
for <KeepAlive> — when re-entered |
onDeactivated |
deactivated |
for <KeepAlive> — when cached & hidden |
onRenderTracked (dev) |
n/a | a reactive dep was tracked during render |
onRenderTriggered (dev) |
n/a | a reactive dep triggered a re-render |
onServerPrefetch |
serverPrefetch |
SSR-only — async data fetch before render |
Show both APIs for the common cases.
<script setup>
import { onMounted, onBeforeUnmount } from "vue";
onMounted(() => {
console.log("mounted, DOM ready");
});
onBeforeUnmount(() => {
cleanup();
});
</script><script>
export default {
mounted() {
console.log("mounted, DOM ready");
},
beforeUnmount() {
this.cleanup();
},
};
</script>When does mounted fire relative to child components?
Child mounted fires before parent mounted. The tree mounts depth-first — leaves first, then their parents. Same for unmounted (reverse: children unmount first).
Parent.beforeMount
Child.beforeMount
Child.mounted ← child first
Parent.mounted ← then parentThis matters when a parent needs to interact with a mounted child (e.g., measure dimensions) — by the time Parent.mounted runs, children are already mounted.
Difference between onUpdated and watch?
onUpdated fires on every re-render of the component, regardless of cause. watch fires when a specific reactive value changes.
// Fires on every render — usually too coarse
onUpdated(() => console.log("re-rendered"));
// Fires only when count changes
watch(count, () => console.log("count changed"));Use onUpdated for things genuinely needing to run on every render (animation tied to layout, after-render DOM measurement); use watch for everything else.
Why does onMounted need to be called synchronously in setup?
Same reason as React hooks: the registration order ties to the lifecycle binding. Calling it inside an if or after an await means it may or may not register on a given setup run, breaking the contract.
// Wrong — conditional registration
if (showFeature) {
onMounted(() => { /* ... */ });
}
// Wrong — after await
const data = await fetch(...);
// setup already passed
onMounted(() => { /* ... */ });
// Right
onMounted(() => {
if (showFeature) { /* ... */ }
});onErrorCaptured — what’s the contract?
Catches errors thrown synchronously, in async handlers, and in lifecycle hooks of descendants. Returning false stops propagation.
<script setup>
import { onErrorCaptured } from "vue";
onErrorCaptured((err, instance, info) => {
reportError(err, info);
return false; // stop propagation
});
</script>Acts like React’s error boundary. Place at app root + key boundaries. Errors in the current component aren’t caught by its own handler; they’re caught by an ancestor.
onRenderTracked / onRenderTriggered — when do you reach for these?
Dev-only debugging hooks. onRenderTracked fires when a reactive dep is tracked during render; onRenderTriggered fires when a tracked dep is the cause of a re-render. Useful for “why did this component re-render?”
onRenderTriggered((event) => {
console.log("re-render triggered by:", event.key, event.target);
});Modern Vue DevTools surface this graphically; you rarely need these by hand.
Vue 2 → Vue 3 lifecycle renames.
| Vue 2 | Vue 3 |
|---|---|
beforeCreate / created |
replaced by setup() body (Composition) |
beforeDestroy |
beforeUnmount |
destroyed |
unmounted |
| n/a | onErrorCaptured (existed in Vue 2, now in Composition too) |
| n/a | onRenderTracked / onRenderTriggered (Vue 3 new) |
Old names still work in Vue 2; Vue 3 supports both old and new (with beforeDestroy warning as deprecated in some setups).
SSR-specific: onServerPrefetch.
Async data fetch that runs during server rendering. Component waits for the returned promise before rendering. Used in plain Vue SSR; Nuxt has its own useAsyncData/useFetch higher-level APIs.
const post = ref(null);
onServerPrefetch(async () => {
post.value = await fetchPost();
});In Composition API + <script setup>, top-level await also serves this purpose (turning the component into an async component, requires <Suspense> upstream).
Gotchas
- Don’t put async code in
mountedand assume the DOM is stable — by the time the await resolves, the component may have re-rendered or unmounted. CheckisMounted/ abort signal. onMounteddoesn’t fire during SSR. Components mount on the server but lifecycle hooks tied to the browser don’t run (or run on hydration on the client). For SSR data, useonServerPrefetch.onUpdatedandnextTick— DOM updates are batched and async. To read post-update DOM from an event handler,await nextTick().<KeepAlive>changes the lifecycle — components inside aren’t unmounted on hide; they getonDeactivated/onActivatedinstead. Long-running effects need to pause/resume accordingly.- Composables registering hooks —
onMountedinside a composable is bound to the consuming component. The composable runs in the consumer’ssetup, registering the hook there. - Errors in
setupitself are NOT caught byonErrorCapturedof the same component — they propagate to the parent’sonErrorCaptured.
Related
Further reading
- Vue docs — Lifecycle Hooks: https://vuejs.org/guide/essentials/lifecycle.html
- Vue API — Reactivity & Lifecycle: https://vuejs.org/api/composition-api-lifecycle.html
Interview angle 5
- “Which lifecycle hook for what?” -
onMountedfor anything needing the DOM,onBeforeUnmountfor teardown,onErrorCapturedfor an error boundary, andwatchwhen you are actually reacting to one value rather than to rendering.onUpdatedonly when you genuinely need every render, which is rare and usually a smell. - “In what order do parent and child mount?” - depth-first: children mount
before parents. So a parent’s
onMountedcan safely assume its children are mounted, and a child’s cannot assume anything about the parent. - “Why must Composition hooks be registered synchronously in
setup?” - because Vue binds them to the current component instance, and that binding only exists during synchronous setup. Registering one after anawaitattaches it to nothing - the same constraint as React’s hook rules, for a similar reason. - “How do you fetch data for SSR?” -
onServerPrefetch, or top-levelawaitin<script setup>.onMountednever runs on the server, so anything in it is client-only by definition - which is the right place for browser APIs and the wrong place for data the HTML should contain. - “What changes under
<KeepAlive>?” - the component is cached rather than destroyed, so you getdeactivatedandactivatedinstead ofunmountedandmounted. Effects need to pause and resume rather than tear down and rebuild, and code that only handles unmount leaks timers and subscriptions.