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.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author