Lifecycle Hooks: Component Birth to Death! ๐ โ
Every Vue component has a lifecycle - it's born, grows up, changes, and eventually gets destroyed. Lifecycle hooks are special functions that run at specific moments in this journey, letting you add your own code at exactly the right time. Think of them as event handlers for your component's life events!
What You'll Master Today โ
- Component lifecycle phases - the journey from creation to destruction
- Essential lifecycle hooks -
onMounted,onUpdated,onUnmounted, and more - When to use each hook - timing your code perfectly
- Real-world scenarios - practical applications of lifecycle hooks
- Cleanup patterns - preventing memory leaks and resource issues
The Component Lifecycle Journey โ
๐ฑ Creation Phase โ
Component is being born and set up
- beforeCreate โ Basic instance initialization
- created โ Data observation and event setup
- onBeforeMount โ Just before mounting to DOM
- onMounted โ Component is in the DOM and ready
๐ Update Phase โ
Component reacts to data changes
- onBeforeUpdate โ Before re-rendering
- onUpdated โ After re-rendering
๐ Destruction Phase โ
Component is being removed
- onBeforeUnmount โ Cleanup time!
- onUnmounted โ Component is gone
Complete Lifecycle Demo Application โ
Let's build a comprehensive app that demonstrates all lifecycle hooks in action:
<!-- App.vue - Lifecycle Demo Controller -->
<template>
<div id="app">
<h1>๐ Vue Lifecycle Hooks Demo</h1>
<!-- Component Controls -->
<div class="controls">
<button @click="showComponent = !showComponent" class="toggle-btn">
{{ showComponent ? '๐ Destroy Component' : '๐ฑ Create Component' }}
</button>
<button @click="updateCounter++" class="update-btn" :disabled="!showComponent">
๐ Trigger Update ({{ updateCounter }})
</button>
<button @click="resetDemo" class="reset-btn">
๐ Reset Demo
</button>
</div>
<!-- Lifecycle Event Log -->
<div class="lifecycle-log">
<h2>๐ Lifecycle Event Log</h2>
<button @click="clearLog" class="clear-log-btn">Clear Log</button>
<div class="log-container">
<div
v-for="(event, index) in lifecycleEvents"
:key="index"
:class="['log-entry', `log-${event.type}`]"
>
<span class="log-time">{{ event.timestamp }}</span>
<span class="log-hook">{{ event.hook }}</span>
<span class="log-message">{{ event.message }}</span>
</div>
</div>
</div>
<!-- The Component Being Demonstrated -->
<div class="component-container">
<h2>๐ญ Demo Component</h2>
<LifecycleDemo
v-if="showComponent"
:update-trigger="updateCounter"
:demo-data="demoData"
@lifecycle-event="logLifecycleEvent"
@data-loaded="handleDataLoaded"
@cleanup-complete="handleCleanupComplete"
/>
<div v-else class="component-placeholder">
<p>๐ Component is destroyed</p>
<p>Click "Create Component" to see it come to life!</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import LifecycleDemo from './components/LifecycleDemo.vue'
// App state
const showComponent = ref(true)
const updateCounter = ref(0)
const lifecycleEvents = ref([])
const demoData = reactive({
user: { name: 'Vue Developer', id: 1 },
preferences: { theme: 'light', notifications: true },
lastUpdate: Date.now()
})
// Methods
const logLifecycleEvent = (event) => {
lifecycleEvents.value.push({
...event,
timestamp: new Date().toLocaleTimeString()
})
// Keep only last 50 events
if (lifecycleEvents.value.length > 50) {
lifecycleEvents.value = lifecycleEvents.value.slice(-50)
}
}
const clearLog = () => {
lifecycleEvents.value = []
}
const resetDemo = () => {
showComponent.value = false
updateCounter.value = 0
lifecycleEvents.value = []
demoData.lastUpdate = Date.now()
// Recreate component after a brief delay
setTimeout(() => {
showComponent.value = true
}, 100)
}
const handleDataLoaded = (data) => {
console.log('๐ Parent received data loaded event:', data)
}
const handleCleanupComplete = () => {
console.log('๐งน Parent received cleanup complete event')
}
</script>
<style>
#app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
h1 {
text-align: center;
color: #2c3e50;
margin-bottom: 30px;
}
.controls {
display: flex;
gap: 15px;
justify-content: center;
margin-bottom: 30px;
flex-wrap: wrap;
}
.controls button {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 16px;
transition: all 0.2s;
}
.toggle-btn {
background: #e74c3c;
color: white;
}
.toggle-btn:hover {
background: #c0392b;
}
.update-btn {
background: #3498db;
color: white;
}
.update-btn:hover:not(:disabled) {
background: #2980b9;
}
.update-btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.reset-btn {
background: #95a5a6;
color: white;
}
.reset-btn:hover {
background: #7f8c8d;
}
.lifecycle-log {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
border: 2px solid #e9ecef;
}
.lifecycle-log h2 {
margin-top: 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.clear-log-btn {
padding: 6px 12px;
background: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.log-container {
max-height: 300px;
overflow-y: auto;
background: white;
border-radius: 4px;
padding: 10px;
}
.log-entry {
display: flex;
gap: 15px;
padding: 8px;
border-bottom: 1px solid #e9ecef;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.log-time {
color: #6c757d;
min-width: 80px;
}
.log-hook {
font-weight: bold;
min-width: 120px;
}
.log-creation { color: #28a745; }
.log-update { color: #007bff; }
.log-destruction { color: #dc3545; }
.log-effect { color: #6f42c1; }
.component-container {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.component-placeholder {
text-align: center;
padding: 40px;
color: #6c757d;
background: #f8f9fa;
border-radius: 8px;
border: 2px dashed #dee2e6;
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
align-items: center;
}
.controls button {
width: 200px;
}
}
</style>Now let's create the demo component that showcases all lifecycle hooks:
<!-- LifecycleDemo.vue - The Component Being Demonstrated -->
<template>
<div class="lifecycle-demo">
<h3>๐ฏ I am the Demo Component!</h3>
<!-- Component Status -->
<div class="status-section">
<div class="status-card">
<h4>๐ Component Status</h4>
<div class="status-grid">
<div class="status-item">
<span class="status-label">Mounted:</span>
<span class="status-value">{{ isMounted ? 'โ
' : 'โ' }}</span>
</div>
<div class="status-item">
<span class="status-label">Data Loaded:</span>
<span class="status-value">{{ isDataLoaded ? 'โ
' : 'โ' }}</span>
</div>
<div class="status-item">
<span class="status-label">Updates:</span>
<span class="status-value">{{ updateCount }}</span>
</div>
<div class="status-item">
<span class="status-label">API Calls:</span>
<span class="status-value">{{ apiCallCount }}</span>
</div>
</div>
</div>
</div>
<!-- Interactive Elements -->
<div class="interactive-section">
<h4>๐ฎ Interactive Elements</h4>
<div class="controls-grid">
<button @click="localCounter++" class="local-btn">
Local Counter: {{ localCounter }}
</button>
<button @click="fetchData" class="fetch-btn" :disabled="isLoading">
{{ isLoading ? 'Loading...' : 'Fetch Data' }}
</button>
<button @click="startTimer" class="timer-btn" :disabled="timerActive">
{{ timerActive ? `Timer: ${timerCount}s` : 'Start Timer' }}
</button>
<button @click="addEventListeners" class="events-btn">
Add Window Events
</button>
</div>
</div>
<!-- Data Display -->
<div class="data-section">
<h4>๐ฆ Component Data</h4>
<div class="data-display">
<div class="data-item">
<strong>Update Trigger:</strong> {{ updateTrigger }}
</div>
<div class="data-item">
<strong>Demo Data:</strong> {{ JSON.stringify(demoData, null, 2) }}
</div>
<div class="data-item">
<strong>Fetched Data:</strong>
<pre v-if="fetchedData">{{ JSON.stringify(fetchedData, null, 2) }}</pre>
<span v-else>No data loaded</span>
</div>
</div>
</div>
<!-- Resource Monitor -->
<div class="resources-section">
<h4>๐ง Resources & Cleanup</h4>
<div class="resources-list">
<div class="resource-item">
<span>๐ Timer:</span>
<span :class="{ active: timerActive }">{{ timerActive ? 'Active' : 'Inactive' }}</span>
</div>
<div class="resource-item">
<span>๐ Event Listeners:</span>
<span :class="{ active: hasEventListeners }">{{ eventListenerCount }} active</span>
</div>
<div class="resource-item">
<span>๐ API Subscriptions:</span>
<span :class="{ active: hasApiSubscription }">{{ hasApiSubscription ? 'Active' : 'None' }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {
ref,
reactive,
computed,
watch,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated,
onErrorCaptured
} from 'vue'
// Props
const props = defineProps({
updateTrigger: {
type: Number,
default: 0
},
demoData: {
type: Object,
default: () => ({})
}
})
// Events
const emit = defineEmits([
'lifecycle-event',
'data-loaded',
'cleanup-complete'
])
// Local state
const isMounted = ref(false)
const isDataLoaded = ref(false)
const updateCount = ref(0)
const apiCallCount = ref(0)
const localCounter = ref(0)
const isLoading = ref(false)
const fetchedData = ref(null)
const timerActive = ref(false)
const timerCount = ref(0)
const eventListenerCount = ref(0)
const hasEventListeners = ref(false)
const hasApiSubscription = ref(false)
// Resource references for cleanup
let timerId = null
let apiSubscription = null
const eventListeners = []
// Helper function to emit lifecycle events
const emitLifecycleEvent = (hook, message, type = 'creation') => {
const event = {
hook,
message,
type,
componentId: 'LifecycleDemo'
}
emit('lifecycle-event', event)
console.log(`๐ ${hook}: ${message}`)
}
// ๐ฑ CREATION LIFECYCLE HOOKS
onBeforeMount(() => {
emitLifecycleEvent(
'onBeforeMount',
'Component is about to be mounted to the DOM',
'creation'
)
})
onMounted(() => {
isMounted.value = true
emitLifecycleEvent(
'onMounted',
'Component has been mounted to the DOM and is ready for interaction',
'creation'
)
// Simulate initial data loading
setTimeout(() => {
loadInitialData()
}, 500)
// Set up any DOM-dependent features
setupDOMFeatures()
})
// ๐ UPDATE LIFECYCLE HOOKS
onBeforeUpdate(() => {
emitLifecycleEvent(
'onBeforeUpdate',
'Component is about to re-render due to reactive data changes',
'update'
)
})
onUpdated(() => {
updateCount.value++
emitLifecycleEvent(
'onUpdated',
`Component has re-rendered (Update #${updateCount.value})`,
'update'
)
})
// ๐ DESTRUCTION LIFECYCLE HOOKS
onBeforeUnmount(() => {
emitLifecycleEvent(
'onBeforeUnmount',
'Component is about to be destroyed - cleanup time!',
'destruction'
)
// Start cleanup process
performCleanup()
})
onUnmounted(() => {
emitLifecycleEvent(
'onUnmounted',
'Component has been completely destroyed and removed from DOM',
'destruction'
)
emit('cleanup-complete')
})
// ๐ญ SPECIAL LIFECYCLE HOOKS (for keep-alive components)
onActivated(() => {
emitLifecycleEvent(
'onActivated',
'Component was activated (used with <keep-alive>)',
'effect'
)
})
onDeactivated(() => {
emitLifecycleEvent(
'onDeactivated',
'Component was deactivated (used with <keep-alive>)',
'effect'
)
})
// ๐จ ERROR HANDLING HOOK
onErrorCaptured((error, instance, errorInfo) => {
emitLifecycleEvent(
'onErrorCaptured',
`Caught error: ${error.message}`,
'effect'
)
console.error('Component error captured:', { error, instance, errorInfo })
return false // Prevent error from propagating
})
// ๐ฏ COMPONENT METHODS
const loadInitialData = async () => {
isLoading.value = true
apiCallCount.value++
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000))
fetchedData.value = {
timestamp: new Date().toISOString(),
userId: props.demoData.user?.id || 'unknown',
preferences: { ...props.demoData.preferences },
randomData: Math.random()
}
isDataLoaded.value = true
emit('data-loaded', fetchedData.value)
emitLifecycleEvent(
'Data Loaded',
'Initial data has been successfully loaded',
'effect'
)
} catch (error) {
console.error('Failed to load data:', error)
} finally {
isLoading.value = false
}
}
const fetchData = async () => {
isLoading.value = true
apiCallCount.value++
try {
await new Promise(resolve => setTimeout(resolve, 800))
fetchedData.value = {
...fetchedData.value,
lastFetch: new Date().toISOString(),
fetchCount: apiCallCount.value,
randomValue: Math.floor(Math.random() * 1000)
}
emitLifecycleEvent(
'Data Fetched',
`Data refreshed (API call #${apiCallCount.value})`,
'effect'
)
} finally {
isLoading.value = false
}
}
const startTimer = () => {
if (timerActive.value) return
timerActive.value = true
timerCount.value = 0
timerId = setInterval(() => {
timerCount.value++
if (timerCount.value >= 10) {
clearInterval(timerId)
timerActive.value = false
timerId = null
}
}, 1000)
emitLifecycleEvent(
'Timer Started',
'Background timer has been started',
'effect'
)
}
const addEventListeners = () => {
const handleResize = () => {
console.log('Window resized!')
}
const handleScroll = () => {
console.log('Window scrolled!')
}
window.addEventListener('resize', handleResize)
window.addEventListener('scroll', handleScroll)
eventListeners.push(
{ event: 'resize', handler: handleResize },
{ event: 'scroll', handler: handleScroll }
)
eventListenerCount.value = eventListeners.length
hasEventListeners.value = true
emitLifecycleEvent(
'Events Added',
`Added ${eventListeners.length} window event listeners`,
'effect'
)
}
const setupDOMFeatures = () => {
// Example of DOM-dependent setup that needs to happen after mounting
emitLifecycleEvent(
'DOM Setup',
'DOM-dependent features have been initialized',
'effect'
)
}
const performCleanup = () => {
// Clear timer
if (timerId) {
clearInterval(timerId)
timerId = null
timerActive.value = false
}
// Remove event listeners
eventListeners.forEach(({ event, handler }) => {
window.removeEventListener(event, handler)
})
eventListeners.length = 0
eventListenerCount.value = 0
hasEventListeners.value = false
// Cancel API subscription if exists
if (apiSubscription) {
apiSubscription.unsubscribe()
apiSubscription = null
hasApiSubscription.value = false
}
emitLifecycleEvent(
'Cleanup Complete',
'All resources have been cleaned up properly',
'destruction'
)
}
// ๐ WATCHERS (also part of component lifecycle)
watch(
() => props.updateTrigger,
(newValue, oldValue) => {
emitLifecycleEvent(
'Prop Changed',
`updateTrigger changed from ${oldValue} to ${newValue}`,
'update'
)
}
)
watch(
() => props.demoData,
(newData) => {
emitLifecycleEvent(
'Data Changed',
'Demo data prop has been updated',
'update'
)
},
{ deep: true }
)
</script>
<style scoped>
.lifecycle-demo {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
}
.lifecycle-demo h3 {
margin-top: 0;
color: #2c3e50;
text-align: center;
}
.status-section,
.interactive-section,
.data-section,
.resources-section {
margin-bottom: 25px;
}
.status-card {
background: white;
padding: 15px;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.status-card h4 {
margin-top: 0;
margin-bottom: 15px;
color: #495057;
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
}
.status-item {
display: flex;
justify-content: space-between;
padding: 8px;
background: #f8f9fa;
border-radius: 4px;
}
.status-label {
font-weight: bold;
}
.status-value {
color: #007bff;
}
.controls-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.controls-grid button {
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.2s;
}
.local-btn {
background: #28a745;
color: white;
}
.local-btn:hover {
background: #218838;
}
.fetch-btn {
background: #007bff;
color: white;
}
.fetch-btn:hover:not(:disabled) {
background: #0056b3;
}
.fetch-btn:disabled {
background: #6c757d;
cursor: not-allowed;
}
.timer-btn {
background: #ffc107;
color: black;
}
.timer-btn:hover:not(:disabled) {
background: #e0a800;
}
.timer-btn:disabled {
background: #6c757d;
color: white;
cursor: not-allowed;
}
.events-btn {
background: #6f42c1;
color: white;
}
.events-btn:hover {
background: #5a32a3;
}
.data-display {
background: white;
padding: 15px;
border-radius: 6px;
border: 1px solid #dee2e6;
}
.data-item {
margin-bottom: 10px;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.data-item strong {
color: #495057;
}
.data-item pre {
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
margin: 5px 0;
overflow-x: auto;
}
.resources-list {
background: white;
padding: 15px;
border-radius: 6px;
border: 1px solid #dee2e6;
}
.resource-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #e9ecef;
}
.resource-item:last-child {
border-bottom: none;
}
.resource-item span.active {
color: #28a745;
font-weight: bold;
}
@media (max-width: 768px) {
.status-grid,
.controls-grid {
grid-template-columns: 1fr;
}
}
</style>Understanding Each Lifecycle Hook โ
๐ฑ Creation Hooks โ
onBeforeMount โ
When: Just before the component is mounted to the DOM Use for: Last-minute setup before DOM creation
onBeforeMount(() => {
console.log('About to mount - DOM not ready yet')
// Final preparations before mounting
})onMounted โญ (Most Important) โ
When: Component is mounted and DOM is available Use for: DOM manipulation, API calls, setting up subscriptions
onMounted(() => {
console.log('Component mounted - DOM is ready!')
// Set up charts, initialize libraries, fetch data
fetchInitialData()
setupEventListeners()
})๐ Update Hooks โ
onBeforeUpdate โ
When: Before component re-renders Use for: Accessing DOM before changes
onBeforeUpdate(() => {
console.log('About to update - current DOM state accessible')
// Capture scroll position, etc.
})onUpdated โ
When: After component re-renders Use for: DOM operations after updates
onUpdated(() => {
console.log('Component updated - new DOM state available')
// Update chart data, scroll to new content
})๐ Destruction Hooks โ
onBeforeUnmount โญ (Critical for Cleanup) โ
When: Just before component is destroyed Use for: Cleanup to prevent memory leaks
onBeforeUnmount(() => {
console.log('About to unmount - cleanup time!')
// Clear timers, remove event listeners, cancel API calls
clearInterval(timerId)
window.removeEventListener('scroll', scrollHandler)
})onUnmounted โ
When: Component is completely destroyed Use for: Final cleanup tasks
onUnmounted(() => {
console.log('Component unmounted and destroyed')
// Analytics, logging, final notifications
})Real-World Lifecycle Patterns โ
1. Data Fetching Pattern โ
const userData = ref(null)
const isLoading = ref(false)
const error = ref(null)
onMounted(async () => {
isLoading.value = true
try {
userData.value = await fetchUserData()
} catch (err) {
error.value = err.message
} finally {
isLoading.value = false
}
})2. Event Listener Pattern โ
onMounted(() => {
const handleScroll = () => {
// Handle scroll
}
window.addEventListener('scroll', handleScroll)
// Cleanup in beforeUnmount
onBeforeUnmount(() => {
window.removeEventListener('scroll', handleScroll)
})
})3. Timer/Interval Pattern โ
let intervalId = null
onMounted(() => {
intervalId = setInterval(() => {
// Update something every second
updateTime()
}, 1000)
})
onBeforeUnmount(() => {
if (intervalId) {
clearInterval(intervalId)
}
})4. Third-Party Library Integration โ
import Chart from 'chart.js'
let chartInstance = null
onMounted(() => {
const canvas = document.getElementById('myChart')
chartInstance = new Chart(canvas, {
type: 'line',
data: chartData.value
})
})
onUpdated(() => {
if (chartInstance) {
chartInstance.data = chartData.value
chartInstance.update()
}
})
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.destroy()
}
})5. WebSocket Connection Pattern โ
let socket = null
onMounted(() => {
socket = new WebSocket('ws://localhost:8080')
socket.onopen = () => {
console.log('WebSocket connected')
}
socket.onmessage = (event) => {
handleMessage(JSON.parse(event.data))
}
})
onBeforeUnmount(() => {
if (socket) {
socket.close()
}
})Advanced Lifecycle Concepts โ
1. Keep-Alive Hooks โ
For components wrapped in <keep-alive>:
onActivated(() => {
console.log('Component activated (shown)')
// Refresh data, resume timers
})
onDeactivated(() => {
console.log('Component deactivated (hidden)')
// Pause expensive operations
})2. Error Boundary Hook โ
onErrorCaptured((error, instance, errorInfo) => {
console.error('Child component error:', error)
// Log to error service
logError(error, instance, errorInfo)
// Return false to stop error propagation
return false
})3. Synchronous Hook Registration โ
// โ
Correct: Synchronous registration
onMounted(() => {
console.log('This works!')
})
// โ Wrong: Asynchronous registration
setTimeout(() => {
onMounted(() => {
console.log('This won\'t work!')
})
}, 100)
### 3. **Synchronous Hook Registration**
```javascript
// โ
Correct: Synchronous registration
onMounted(() => {
console.log('This works!')
})
// โ Wrong: Asynchronous registration
setTimeout(() => {
onMounted(() => {
console.log('This won\'t work!')
})
}, 100)
// โ
Correct: Hooks in external functions (if called synchronously)
function useDataFetching() {
onMounted(() => {
fetchData()
})
}
// Call synchronously in setup
useDataFetching() // This works!Common Lifecycle Patterns and Use Cases โ
1. Dashboard Component with Real-Time Data โ
const dashboardData = ref(null)
const isConnected = ref(false)
let socket = null
let refreshInterval = null
onMounted(() => {
// Initial data fetch
fetchDashboardData()
// Set up real-time updates
connectWebSocket()
// Periodic refresh as fallback
refreshInterval = setInterval(fetchDashboardData, 30000)
})
onBeforeUnmount(() => {
// Clean up all resources
if (socket) {
socket.close()
}
if (refreshInterval) {
clearInterval(refreshInterval)
}
})
const connectWebSocket = () => {
socket = new WebSocket('ws://api.example.com/dashboard')
socket.onopen = () => {
isConnected.value = true
}
socket.onmessage = (event) => {
dashboardData.value = JSON.parse(event.data)
}
socket.onclose = () => {
isConnected.value = false
}
}2. Form Component with Auto-Save โ
const formData = ref({})
const hasUnsavedChanges = ref(false)
let saveTimer = null
onMounted(() => {
// Load saved form data
loadFormData()
// Set up auto-save
watch(formData, () => {
hasUnsavedChanges.value = true
debouncedSave()
}, { deep: true })
// Warn before leaving page with unsaved changes
window.addEventListener('beforeunload', handleBeforeUnload)
})
onBeforeUnmount(() => {
// Save any pending changes
if (hasUnsavedChanges.value) {
saveFormData()
}
// Clean up
if (saveTimer) {
clearTimeout(saveTimer)
}
window.removeEventListener('beforeunload', handleBeforeUnload)
})
const debouncedSave = () => {
if (saveTimer) {
clearTimeout(saveTimer)
}
saveTimer = setTimeout(() => {
saveFormData()
}, 2000)
}
const handleBeforeUnload = (event) => {
if (hasUnsavedChanges.value) {
event.preventDefault()
event.returnValue = 'You have unsaved changes!'
}
}3. Chart Component with Responsive Updates โ
import { Chart } from 'chart.js'
const chartData = ref([])
let chartInstance = null
let resizeObserver = null
onMounted(() => {
initializeChart()
setupResizeObserver()
})
onUpdated(() => {
updateChartData()
})
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.destroy()
}
if (resizeObserver) {
resizeObserver.disconnect()
}
})
const initializeChart = () => {
const canvas = document.getElementById('chart')
chartInstance = new Chart(canvas, {
type: 'line',
data: {
datasets: [{
data: chartData.value
}]
},
options: {
responsive: true,
maintainAspectRatio: false
}
})
}
const setupResizeObserver = () => {
const chartContainer = document.querySelector('.chart-container')
resizeObserver = new ResizeObserver(() => {
if (chartInstance) {
chartInstance.resize()
}
})
resizeObserver.observe(chartContainer)
}
const updateChartData = () => {
if (chartInstance) {
chartInstance.data.datasets[0].data = chartData.value
chartInstance.update()
}
}4. Video Player Component โ
const videoRef = ref(null)
const isPlaying = ref(false)
const currentTime = ref(0)
let progressInterval = null
onMounted(() => {
const video = videoRef.value
// Set up video event listeners
video.addEventListener('play', () => {
isPlaying.value = true
startProgressTracking()
})
video.addEventListener('pause', () => {
isPlaying.value = false
stopProgressTracking()
})
video.addEventListener('ended', () => {
isPlaying.value = false
stopProgressTracking()
})
// Set up keyboard shortcuts
document.addEventListener('keydown', handleKeydown)
})
onBeforeUnmount(() => {
stopProgressTracking()
document.removeEventListener('keydown', handleKeydown)
})
const startProgressTracking = () => {
progressInterval = setInterval(() => {
if (videoRef.value) {
currentTime.value = videoRef.value.currentTime
}
}, 100)
}
const stopProgressTracking = () => {
if (progressInterval) {
clearInterval(progressInterval)
progressInterval = null
}
}
const handleKeydown = (event) => {
if (event.code === 'Space') {
event.preventDefault()
togglePlayPause()
}
}Debugging Lifecycle Hooks โ
1. Lifecycle Event Logger โ
const logLifecycleEvent = (hookName, details = {}) => {
console.log(`๐ ${hookName}`, {
timestamp: new Date().toISOString(),
componentName: 'MyComponent',
...details
})
}
onBeforeMount(() => logLifecycleEvent('onBeforeMount'))
onMounted(() => logLifecycleEvent('onMounted', { domReady: true }))
onBeforeUpdate(() => logLifecycleEvent('onBeforeUpdate'))
onUpdated(() => logLifecycleEvent('onUpdated'))
onBeforeUnmount(() => logLifecycleEvent('onBeforeUnmount'))
onUnmounted(() => logLifecycleEvent('onUnmounted'))2. Performance Monitoring โ
onBeforeMount(() => {
console.time('component-mount')
})
onMounted(() => {
console.timeEnd('component-mount')
// Log component size and performance
const element = document.querySelector('.my-component')
console.log('Component dimensions:', {
width: element.offsetWidth,
height: element.offsetHeight
})
})3. Development vs Production Hooks โ
onMounted(() => {
if (import.meta.env.DEV) {
// Development-only code
console.log('Development mode: Component mounted')
window.debugComponent = getCurrentInstance()
}
// Production code
trackComponentLoad('MyComponent')
})Testing Lifecycle Hooks โ
1. Testing Hook Execution โ
import { mount } from '@vue/test-utils'
import MyComponent from '@/components/MyComponent.vue'
test('calls onMounted hook', () => {
const mockFetch = jest.fn()
global.fetch = mockFetch
mount(MyComponent)
expect(mockFetch).toHaveBeenCalled()
})
test('cleans up resources on unmount', () => {
const clearIntervalSpy = jest.spyOn(global, 'clearInterval')
const wrapper = mount(MyComponent)
wrapper.unmount()
expect(clearIntervalSpy).toHaveBeenCalled()
})2. Testing Async Operations โ
test('loads data on mount', async () => {
const mockData = { id: 1, name: 'Test' }
global.fetch = jest.fn().mockResolvedValue({
json: () => Promise.resolve(mockData)
})
const wrapper = mount(MyComponent)
// Wait for mounted hook and async operations
await wrapper.vm.$nextTick()
await new Promise(resolve => setTimeout(resolve, 0))
expect(wrapper.vm.data).toEqual(mockData)
})Performance Considerations โ
1. Expensive Operations โ
onMounted(() => {
// โ Avoid: Heavy computations in lifecycle hooks
const result = heavyComputation()
// โ
Better: Use nextTick for non-critical operations
nextTick(() => {
heavyComputation()
})
// โ
Best: Use setTimeout for very heavy operations
setTimeout(() => {
heavyComputation()
}, 0)
})2. Memory Leak Prevention โ
const subscriptions = []
onMounted(() => {
// Keep track of all subscriptions
subscriptions.push(
eventBus.on('user-updated', handleUserUpdate),
websocket.subscribe('notifications', handleNotification),
store.watch('theme', handleThemeChange)
)
})
onBeforeUnmount(() => {
// Clean up all subscriptions
subscriptions.forEach(unsubscribe => {
if (typeof unsubscribe === 'function') {
unsubscribe()
}
})
subscriptions.length = 0
})3. Conditional Hook Logic โ
onMounted(() => {
// Only set up expensive features if needed
if (props.enableRealTimeUpdates) {
setupWebSocket()
}
if (props.trackAnalytics) {
setupAnalytics()
}
// Always clean up conditionally created resources
onBeforeUnmount(() => {
if (websocket) {
websocket.close()
}
if (analytics) {
analytics.disconnect()
}
})
})Lifecycle Hooks Cheat Sheet โ
| Hook | When | Common Use Cases |
|---|---|---|
onBeforeMount | Before DOM creation | Last-minute setup |
onMounted โญ | After DOM creation | API calls, DOM setup, libraries |
onBeforeUpdate | Before re-render | Capture current state |
onUpdated | After re-render | DOM updates, scroll position |
onBeforeUnmount โญ | Before destruction | Cleanup timers, events, subscriptions |
onUnmounted | After destruction | Final cleanup, logging |
onActivated | Keep-alive activation | Resume operations |
onDeactivated | Keep-alive deactivation | Pause operations |
onErrorCaptured | Child error | Error handling, logging |
What You've Mastered! ๐ โ
- โ Complete lifecycle understanding - from birth to death of components
- โ Essential hooks - onMounted, onUpdated, onBeforeUnmount usage
- โ Resource management - proper cleanup to prevent memory leaks
- โ Real-world patterns - data fetching, event handling, third-party integration
- โ Performance optimization - efficient lifecycle hook usage
- โ Debugging techniques - monitoring and troubleshooting lifecycle issues
- โ Testing strategies - validating lifecycle behavior in components
Best Practices Summary โ
1. Always Clean Up โ
- Clear timers and intervals in
onBeforeUnmount - Remove event listeners
- Close WebSocket connections
- Cancel pending API requests
2. Use the Right Hook โ
- Data fetching โ
onMounted - DOM manipulation โ
onMountedoronUpdated - Resource cleanup โ
onBeforeUnmount - Third-party setup โ
onMounted
3. Keep Hooks Simple โ
- Extract complex logic into separate functions
- Use async/await for readability
- Handle errors gracefully
- Log important lifecycle events
4. Performance Matters โ
- Avoid heavy computations in lifecycle hooks
- Use
nextTick()for non-critical operations - Clean up resources to prevent memory leaks
- Monitor performance in development
What's Next? โ
In the next project, you'll learn about slots and content distribution - Vue's powerful system for creating flexible, reusable components that can accept and display dynamic content. This will teach you how to build truly composable components!
Quick Reference โ
Essential Lifecycle Hooks โ
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
// Setup phase
onBeforeMount(() => { /* Final preparations */ })
onMounted(() => { /* DOM ready, fetch data, setup */ })
// Update phase
onBeforeUpdate(() => { /* Before re-render */ })
onUpdated(() => { /* After re-render */ })
// Cleanup phase
onBeforeUnmount(() => { /* Clean up resources */ })
onUnmounted(() => { /* Final cleanup */ })Common Cleanup Pattern โ
let timer = null
let subscription = null
onMounted(() => {
timer = setInterval(updateData, 1000)
subscription = eventBus.subscribe('update', handler)
})
onBeforeUnmount(() => {
if (timer) clearInterval(timer)
if (subscription) subscription.unsubscribe()
})You now understand the complete Vue component lifecycle and can build robust, leak-free applications! ๐