frontend / vue / 06_lifecycle_hooks.md

Lifecycle Hooks (Composition + Options Side by Side)

5 min read source

Lifecycle Hooks (Composition + Options Side by Side)

TL;DR

Same lifecycle, two APIs. Composition API uses imported functions inside setup (onMounted(() => ...)); Options API uses named options (mounted() { ... }). Vue 3 renamed beforeDestroy/destroyedbeforeUnmount/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.

Interview Q&A

Q: List the lifecycle hooks and what each is for.

A:

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

Q: Show both APIs for the common cases.

A:

<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>

Q: When does mounted fire relative to child components?

A: 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 parent

This 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.

Q: Difference between onUpdated and watch?

A: 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.

Q: Why does onMounted need to be called synchronously in setup?

A: 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(...);
onMounted(() => { /* ... */ });    // setup already passed

// Right
onMounted(() => {
  if (showFeature) { /* ... */ }
});

Q: onErrorCaptured — what’s the contract?

A: 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.

Q: onRenderTracked / onRenderTriggered — when do you reach for these?

A: 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.

Q: Vue 2 → Vue 3 lifecycle renames.

A:

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).

Q: SSR-specific: onServerPrefetch.

A: 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 / edge cases

  • Don’t put async code in mounted and assume the DOM is stable — by the time the await resolves, the component may have re-rendered or unmounted. Check isMounted / abort signal.
  • onMounted doesn’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, use onServerPrefetch.
  • onUpdated and nextTick — 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 get onDeactivated/onActivated instead. Long-running effects need to pause/resume accordingly.
  • Composables registering hooksonMounted inside a composable is bound to the consuming component. The composable runs in the consumer’s setup, registering the hook there.
  • Errors in setup itself are NOT caught by onErrorCaptured of the same component — they propagate to the parent’s onErrorCaptured.

What a senior is expected to say

  • onMounted for DOM-ready work, onBeforeUnmount for cleanup, onErrorCaptured for error boundaries, watch for specific value reactions, onUpdated only when you genuinely need every render.”
  • “Mount order is depth-first: children mount before parents. So a parent’s onMounted sees fully-mounted children.”
  • “Composition hooks register during setup, synchronously — same hook-rule constraint as React, same reason.”
  • onServerPrefetch (or top-level await in <script setup>) for SSR data; client-only effects belong in onMounted which doesn’t fire on the server.”
  • <KeepAlive> swaps unmounted for deactivated — your effects need to pause/resume, not setup/teardown.”

Cross-references

Further reading