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 usesonMounted- same timing, different syntax - DOM does not exist until
mounted/onMounted- accessing$refsincreatedreturns undefined - Use hooks for side effects (API calls, timers, DOM setup); use
watch/watchEffectfor reactive logic - SSR skips
beforeMount/mountedentirely - put shared server/client logic increated/setup
Quick Example
// 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 → 6data() 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.
// 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) oronMounted(Composition).createdstarts the request before DOM renders;onMountedis safer for SSR and covers most cases - DOM access:
mounted/onMountedonly. 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/onUpdatedor better,watchEffect. Avoid heavy logic inupdatedsince it fires on every reactive change - SSR-safe logic:
created/setuprun on both server and client;mountedruns 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
// 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
// Wrong - interval keeps running after component is gone
mounted() {
this.timer = setInterval(() => fetchUpdates(), 5000);
}
// Fix
beforeUnmount() {
clearInterval(this.timer);
}API fetch in beforeMount
// 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
// 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:
- Parent
beforeUpdate - Child
beforeUpdate - Child
updated - 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:
asyncDataruns increatedfor SSR data fetching before hydration - Pinia:
beforeUnmountremoves store subscriptions to prevent duplicate handlers - Element Plus:
mountedinitializes tooltip positioning after DOM is available - Quasar:
onBeforeUnmountstops media query listeners in layout components - Fetch abort pattern: store
AbortControllerinsetup, cancel inonBeforeUnmount
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
// 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
<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
// 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 updatedParent'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 readyA concise answer to help you respond confidently on this topic during an interview.