How to debug Vue lifecycle hooks
Debugging Vue lifecycle hooks helps understand component behavior, timing issues, and execution order during initialization and updates. As the creator of CoreUI with over 12 years of Vue.js experience since 2014, I’ve debugged countless lifecycle issues in complex applications. Vue lifecycle hooks execute at specific moments in component lifecycle providing insight into mounting, updating, and unmounting phases. This approach reveals timing issues, state problems, and helps optimize component performance.
Use console logging, Vue DevTools, and lifecycle hook tracing to debug component initialization, updates, and cleanup.
Basic lifecycle debugging:
<script setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
ref
} from 'vue'
const count = ref(0)
onBeforeMount(() => {
console.log('onBeforeMount - component not yet in DOM')
console.log('count:', count.value)
})
onMounted(() => {
console.log('onMounted - component is now in DOM')
console.log('DOM element:', document.querySelector('.counter'))
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate - before reactive data changes update DOM')
console.log('current count:', count.value)
})
onUpdated(() => {
console.log('onUpdated - after DOM has been updated')
console.log('new count:', count.value)
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount - component still functional')
})
onUnmounted(() => {
console.log('onUnmounted - component destroyed')
})
</script>
<template>
<div class="counter">
<p>Count: {{ count }}</p>
<button @click="count++">Increment</button>
</div>
</template>
Lifecycle debugger composable:
// useLifecycleDebugger.js
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
getCurrentInstance
} from 'vue'
export function useLifecycleDebugger(componentName) {
const instance = getCurrentInstance()
const log = (hook, data = {}) => {
console.group(`[${componentName}] ${hook}`)
console.log('Timestamp:', new Date().toISOString())
console.log('Instance:', instance)
console.log('Data:', data)
console.trace('Call stack')
console.groupEnd()
}
onBeforeMount(() => log('onBeforeMount'))
onMounted(() => log('onMounted'))
onBeforeUpdate(() => log('onBeforeUpdate'))
onUpdated(() => log('onUpdated'))
onBeforeUnmount(() => log('onBeforeUnmount'))
onUnmounted(() => log('onUnmounted'))
return { log }
}
// Usage in component
import { useLifecycleDebugger } from './useLifecycleDebugger'
const { log } = useLifecycleDebugger('UserProfile')
Tracking state changes:
<script setup>
import { ref, watch, watchEffect, onMounted } from 'vue'
const user = ref({ name: 'John', age: 30 })
const posts = ref([])
// Watch specific value
watch(() => user.value.name, (newName, oldName) => {
console.log('Name changed:', { oldName, newName })
console.trace('Change triggered from:')
})
// Watch entire object
watch(user, (newUser, oldUser) => {
console.log('User object changed:', {
old: oldUser,
new: newUser
})
}, { deep: true })
// Track all reactive dependencies
watchEffect(() => {
console.log('watchEffect triggered')
console.log('Current user:', user.value)
console.log('Posts count:', posts.value.length)
})
onMounted(() => {
console.log('Component mounted with initial state:', {
user: user.value,
posts: posts.value
})
})
</script>
Performance timing:
<script setup>
import { onMounted, onUpdated, ref } from 'vue'
const items = ref([])
let mountTime = 0
let updateCount = 0
onMounted(() => {
mountTime = performance.now()
console.log(`Component mounted in ${mountTime.toFixed(2)}ms`)
// Measure DOM operations
performance.mark('mount-complete')
performance.measure('component-mount', 'mount-complete')
const measures = performance.getEntriesByType('measure')
console.log('Performance measures:', measures)
})
onUpdated(() => {
updateCount++
const updateTime = performance.now() - mountTime
console.log(`Update #${updateCount} at ${updateTime.toFixed(2)}ms`)
// Check for excessive updates
if (updateCount > 10) {
console.warn('Component updating frequently - possible performance issue')
}
})
const addItem = () => {
const start = performance.now()
items.value.push({ id: Date.now(), name: `Item ${items.value.length}` })
const duration = performance.now() - start
console.log(`Add item took ${duration.toFixed(2)}ms`)
}
</script>
Async operations debugging:
<script setup>
import { ref, onMounted } from 'vue'
const data = ref(null)
const loading = ref(false)
const error = ref(null)
async function fetchData() {
console.log('fetchData called')
loading.value = true
try {
console.log('Starting API call...')
const response = await fetch('/api/data')
console.log('Response received:', response.status)
data.value = await response.json()
console.log('Data parsed:', data.value)
} catch (err) {
console.error('Error fetching data:', err)
error.value = err
} finally {
loading.value = false
console.log('fetchData complete')
}
}
onMounted(() => {
console.log('onMounted - calling fetchData')
fetchData()
})
</script>
<template>
<div>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>{{ data }}</div>
</div>
</template>
Parent-child lifecycle order:
<!-- Parent.vue -->
<script setup>
import { onBeforeMount, onMounted } from 'vue'
import Child from './Child.vue'
onBeforeMount(() => console.log('Parent: onBeforeMount'))
onMounted(() => console.log('Parent: onMounted'))
</script>
<template>
<div>
<h1>Parent</h1>
<Child />
</div>
</template>
<!-- Child.vue -->
<script setup>
import { onBeforeMount, onMounted } from 'vue'
onBeforeMount(() => console.log('Child: onBeforeMount'))
onMounted(() => console.log('Child: onMounted'))
</script>
<template>
<div>Child Component</div>
</template>
<!-- Console output:
Parent: onBeforeMount
Child: onBeforeMount
Child: onMounted
Parent: onMounted
-->
DevTools integration:
<script setup>
import { ref, onMounted, customRef } from 'vue'
// Custom ref with debugging
function useDebouncedRef(value, delay = 300) {
return customRef((track, trigger) => {
let timeout
return {
get() {
track()
console.log('Reading value:', value)
return value
},
set(newValue) {
console.log('Setting value:', newValue)
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
console.log('Value updated after debounce')
}, delay)
}
}
})
}
const searchTerm = useDebouncedRef('')
onMounted(() => {
// Add custom data to DevTools
if (import.meta.env.MODE === 'development') {
window.__VUE_DEVTOOLS_GLOBAL_HOOK__?.emit('custom-inspector', {
id: 'lifecycle-debugger',
label: 'Lifecycle Events',
data: {
mounted: Date.now(),
searchTerm: searchTerm.value
}
})
}
})
</script>
Conditional rendering debugging:
<script setup>
import { ref, watch } from 'vue'
const showContent = ref(false)
const contentMountCount = ref(0)
watch(showContent, (newValue, oldValue) => {
console.log('Visibility changed:', { from: oldValue, to: newValue })
if (newValue) {
contentMountCount.value++
console.log(`Content mounted ${contentMountCount.value} times`)
}
})
</script>
<template>
<div>
<button @click="showContent = !showContent">
Toggle Content
</button>
<!-- This component will mount/unmount -->
<div v-if="showContent">
<ContentComponent @mounted="console.log('Content component mounted')" />
</div>
</div>
</template>
Memory leak detection:
<script setup>
import { onMounted, onUnmounted } from 'vue'
let intervalId = null
let eventListeners = []
onMounted(() => {
console.log('Setting up resources...')
// Track interval
intervalId = setInterval(() => {
console.log('Interval tick')
}, 1000)
// Track event listeners
const handleResize = () => console.log('Resized')
window.addEventListener('resize', handleResize)
eventListeners.push({ type: 'resize', handler: handleResize })
console.log('Resources created:', {
interval: !!intervalId,
listeners: eventListeners.length
})
})
onUnmounted(() => {
console.log('Cleaning up resources...')
// Clear interval
if (intervalId) {
clearInterval(intervalId)
console.log('Interval cleared')
}
// Remove event listeners
eventListeners.forEach(({ type, handler }) => {
window.removeEventListener(type, handler)
console.log(`Event listener removed: ${type}`)
})
console.log('Cleanup complete')
})
</script>
Best Practice Note
Use onMounted for DOM operations and API calls. Use onBeforeUnmount to clean up timers and event listeners. Watch reactive values to track state changes. Use performance.mark and performance.measure for timing. Check DevTools Timeline for lifecycle hook execution order. Log component name for easier debugging in large applications. Track mount counts for components that should mount once. Use watchEffect to understand reactive dependencies. This is how we debug lifecycle hooks in CoreUI Vue applications—strategic logging, performance monitoring, and DevTools integration revealing timing issues and helping optimize component behavior.



