Skip to main content

Vue components and their lifecycles

Vue component lifecycle defines the ordered phases every Vue component passes through from creation to removal, with hooks to run code at each point.

Theory

TL;DR

  • Think of it like a restaurant server's shift: hired (beforeCreate/created), starts work (beforeMount/mounted), handles orders (beforeUpdate/updated), clocks out (beforeUnmount/unmounted)
  • Options API uses mounted; Composition API uses onMounted - same timing, different syntax
  • DOM does not exist until mounted/onMounted - accessing $refs in created returns undefined
  • Use hooks for side effects (API calls, timers, DOM setup); use watch/watchEffect for reactive logic
  • SSR skips beforeMount/mounted entirely - put shared server/client logic in created/setup

Quick Example

js
// Options API: lifecycle order on mount then destroy export default { name: 'LifecycleDemo', data() { return { count: 0 }; }, beforeCreate() { console.log('1. beforeCreate - data not ready:', this.count); }, // undefined created() { console.log('2. created - data ready:', this.count); }, // 0 beforeMount() { console.log('3. beforeMount - no DOM yet'); }, mounted() { console.log('4. mounted - DOM accessible'); }, beforeUnmount(){ console.log('5. beforeUnmount - cleanup here'); }, unmounted() { console.log('6. unmounted'); } }; // Console: 1 → 2 → 3 → 4 ... then on destroy: 5 → 6

data() initializes between beforeCreate and created. The DOM only exists from mounted onward.

Options API vs Composition API

Options API hooks (beforeCreate, mounted) sit directly on the component object, like methods on a class. Straightforward, but related logic ends up scattered across separate option blocks in the same file.

Composition API wraps the same timing inside setup() using functions like onMounted and onBeforeUnmount. There is no direct beforeCreate equivalent because setup() itself runs at that moment. The payoff: tree-shakable, reusable composables without this.

js
// Composition API - same timing, different structure import { ref, onMounted, onBeforeUnmount } from 'vue'; export default { setup() { const count = ref(0); onMounted(() => console.log('mounted, count:', count.value)); // 0 onBeforeUnmount(() => console.log('cleaning up')); return { count }; } };

Both APIs produce the same hook timing. The difference is code organization, not behavior.

When to Use Each Hook

  • Initial data fetch: created (Options) or onMounted (Composition). created starts the request before DOM renders; onMounted is safer for SSR and covers most cases
  • DOM access: mounted/onMounted only. The DOM simply does not exist before this point
  • Cleanup on leave: beforeUnmount/onBeforeUnmount. Cancel fetch controllers, clear intervals, remove event listeners here
  • Reacting to changes: updated/onUpdated or better, watchEffect. Avoid heavy logic in updated since it fires on every reactive change
  • SSR-safe logic: created/setup run on both server and client; mounted runs only in the browser

How It Works Internally

During created/setup, Vue scans the template and wraps data/ref properties in Proxy objects to make them reactive. On mount, Vue recursively builds a virtual node (vnode) tree and patches it into real DOM using browser APIs like createElementNS and insertBefore.

When reactive state changes, Vue queues a re-render via queueJob, batching multiple synchronous changes into a single DOM patch. That is why updated fires once even if you change three properties in a row. On unmount, Vue calls unmountComponent, removes the vnodes, and stops all effect watchers.

Common Mistakes

DOM access in created

js
// Wrong created() { this.$refs.box.style.color = 'red'; // TypeError: cannot read properties of undefined } // Fix: move to mounted() mounted() { this.$refs.box.style.color = 'red'; // works }

Missing cleanup on unmount

js
// Wrong - interval keeps running after component is gone mounted() { this.timer = setInterval(() => fetchUpdates(), 5000); } // Fix beforeUnmount() { clearInterval(this.timer); }

API fetch in beforeMount

js
// Wrong - blocks render, breaks SSR hydration async beforeMount() { this.user = await fetchUser(); // Vue 3 warns on async pre-mount hooks } // Fix: use async setup() or onMounted async setup() { const user = await fetchUser(); return { user }; }

Stale $refs in updated

js
// Wrong - child component may not have updated yet updated() { console.log(this.$refs.child.innerHTML); // possibly stale } // Fix updated() { this.$nextTick(() => console.log(this.$refs.child.innerHTML)); // fresh }

Child vs Parent Hook Order

This trips up a lot of developers. When a parent updates a prop, the order is:

  1. Parent beforeUpdate
  2. Child beforeUpdate
  3. Child updated
  4. Parent updated

Parent finishes last. If you read child state from a parent hook, do it in updated, not beforeUpdate. In dynamic lists like TodoMVC with nested components, getting this order wrong produces subtle stale-data bugs.

Real-World Usage

  • Nuxt.js: asyncData runs in created for SSR data fetching before hydration
  • Pinia: beforeUnmount removes store subscriptions to prevent duplicate handlers
  • Element Plus: mounted initializes tooltip positioning after DOM is available
  • Quasar: onBeforeUnmount stops media query listeners in layout components
  • Fetch abort pattern: store AbortController in setup, cancel in onBeforeUnmount

Follow-Up Questions

Q: What is the difference between mounted and nextTick?
A: mounted fires after Vue patches the vnode tree into the DOM, but before the browser paints the frame. nextTick waits for the full update queue to flush, including child component updates. If you need to read a child's DOM after a state change, use nextTick inside updated.

Q: How does SSR affect lifecycle hooks?
A: Vue skips beforeMount and mounted on the server entirely. Only beforeCreate, created, and setup run. Logic that must work on both server and client belongs in setup or created.

Q: What is the Composition API equivalent of beforeCreate?
A: There is no direct equivalent. setup() itself runs at the same time beforeCreate would. Code at the top of setup, before any onX calls, fills that role.

Q: When does activated fire with KeepAlive?
A: When a cached component re-enters the view. First time it fires after mounted; on subsequent shows it fires instead of mounted. Pair activated with deactivated for data refresh logic on cached routes.

Q: In a Teleport component inside Suspense, trace the lifecycle from suspend to resolve.
A: Suspense enters pending state, child setup/created runs, the async dependency resolves, parent beforeUpdate fires, child mounted fires (Teleport changes where DOM lands but not when hooks fire), then parent updated. The lifecycle order stays intact regardless of Teleport's target.

Examples

Basic: Lifecycle hook order in the console

js
// Options API - trace every phase export default { name: 'OrderDemo', data() { return { message: 'hello' }; }, beforeCreate() { console.log('beforeCreate - this.message:', this.message); }, // undefined created() { console.log('created - this.message:', this.message); }, // 'hello' mounted() { console.log('mounted - DOM ready'); }, beforeUpdate() { console.log('beforeUpdate - about to re-render'); }, updated() { console.log('updated - DOM refreshed'); }, beforeUnmount(){ console.log('beforeUnmount - last chance for cleanup'); }, unmounted() { console.log('unmounted'); } };

Change message from a parent component. You will see beforeUpdate and updated fire once per change, not once per property assignment - because Vue batches updates.

Intermediate: Dashboard card with fetch and cleanup

vue
<template> <div v-if="loading">Loading...</div> <div v-else>{{ user.name }}</div> </template> <script> export default { data() { return { user: null, loading: true, controller: null }; }, async mounted() { this.controller = new AbortController(); try { const res = await fetch('/api/profile', { signal: this.controller.signal }); this.user = await res.json(); } catch (e) { if (e.name !== 'AbortError') console.error(e); } finally { this.loading = false; } }, beforeUnmount() { // User navigates away before fetch completes - cancel it this.controller?.abort(); } }; </script>

The AbortController pattern prevents "can't set state on unmounted component" warnings in SPAs where users navigate quickly between routes.

Advanced: Parent-child update ordering edge case

js
// Child logs its own hooks const Child = { props: ['items'], beforeUpdate() { console.log('Child beforeUpdate'); }, updated() { console.log('Child updated'); }, template: '<ul><li v-for="i in items" :key="i">{{ i }}</li></ul>' }; // Parent triggers update by changing items export default { components: { Child }, data() { return { items: [] }; }, mounted() { setTimeout(() => { this.items = [1, 2, 3]; }, 1000); }, beforeUpdate() { console.log('Parent beforeUpdate'); }, updated() { console.log('Parent updated'); }, template: '<Child :items="items" />' }; // Console order: // Parent beforeUpdate // Child beforeUpdate // Child updated // Parent updated

Parent's updated fires last. If you need to read the child's updated DOM from a parent hook, do it in parent updated, not beforeUpdate. Reading too early gives you the pre-update state.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?