Frontend Development 12 min read

Why Using Undocumented @vue:mounted in Vue 3 Is Risky and Better Alternatives

This article examines the undocumented @vue:mounted lifecycle syntax in Vue 3 dynamic components, explains its advantages and risks, compares alternative approaches like emit events, ref access, and provide/inject, and offers practical review recommendations for stable, maintainable code.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Why Using Undocumented @vue:mounted in Vue 3 Is Risky and Better Alternatives

Preface

Hello, I am the reviewer. While doing a code review I found a line of code that uses

<component :is="currentComponent" @vue:mounted="handleMounted" />

. I wondered what syntax this was, thought it resembled the Vue 2

@hook:mounted

syntax, searched the Vue 3 documentation and found nothing, and later learned it is an undocumented feature.

Starting from a Dynamic Component

The requirement is to obtain some information after a child component is loaded, updated, or destroyed. The following example shows a dynamic component that uses the undocumented lifecycle listeners.

<template>
  <div class="demo-container">
    <h2>Dynamic Component Load Monitoring</h2>
    <div class="status">Current component status: {{ componentStatus }}</div>
    <div class="controls">
      <button @click="loadComponent('ComponentA')">Load Component A</button>
      <button @click="loadComponent('ComponentB')">Load Component B</button>
      <button @click="unloadComponent">Unload Component</button>
    </div>
    <!-- Undocumented usage -->
    <component 
      :is="currentComponent" 
      v-if="currentComponent"
      @vue:mounted="handleMounted"
      @vue:updated="handleUpdated"
      @vue:beforeUnmount="handleBeforeUnmount"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'

const currentComponent = ref(null)
const componentStatus = ref('No component')

const handleMounted = () => {
  componentStatus.value = '✅ Component mounted'
  console.log('Component mounted')
}

const handleUpdated = () => {
  componentStatus.value = '🔄 Component updated'
  console.log('Component updated')
}

const handleBeforeUnmount = () => {
  componentStatus.value = '❌ Component about to unmount'
  console.log('Component about to unmount')
}

const loadComponent = (name) => {
  currentComponent.value = name
}

const unloadComponent = () => {
  currentComponent.value = null
  componentStatus.value = 'No component'
}
</script>

The main advantage of using

@vue:mounted

(and the other lifecycle hooks) is that all lifecycle handling logic can be placed in a single place – the parent component – without having to modify each possible child component.

However, the syntax is not documented, which raises concerns about stability and future compatibility.

Deep Dive: Undocumented Feature

Searching the Vue GitHub discussions revealed the following answer from the core team:

"This feature is not designed for user applications, which is why we decided not to document it."

Source: https://github.com/orgs/vuejs/discussions/9298

✅ The feature works and can be used.

❌ It is not guaranteed to be stable.

⚠️ It may be removed in future versions.

🚫 It is not recommended for production use.

The Vue 3 migration guide mentions converting

@hook:

(Vue 2) to

@vue:

(Vue 3) for compatibility, not as an encouraged practice.

Why the code looks fine?

Because all lifecycle listeners are centralized in the parent, the code appears concise and easier to manage.

Review Suggestions

Considering safety and stability, the following solutions are recommended.

Solution 1: Child Component Emits Events (Recommended)

Modify each child component to emit lifecycle events.

<!-- ComponentA.vue -->
<template>
  <div class="component-a">
    <h3>I am Component A</h3>
    <button @click="counter++">Clicks: {{ counter }}</button>
  </div>
</template>

<script setup>
import { ref, onMounted, onUpdated, onBeforeUnmount } from 'vue'

const emit = defineEmits(['lifecycle'])
const counter = ref(0)

onMounted(() => {
  emit('lifecycle', { type: 'mounted', componentName: 'ComponentA' })
})

onUpdated(() => {
  emit('lifecycle', { type: 'updated', componentName: 'ComponentA' })
})

onBeforeUnmount(() => {
  emit('lifecycle', { type: 'beforeUnmount', componentName: 'ComponentA' })
})
</script>
<!-- ComponentB.vue -->
<template>
  <div class="component-b">
    <h3>I am Component B</h3>
    <input v-model="text" placeholder="Enter text" />
    <p>{{ text }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted, onUpdated, onBeforeUnmount } from 'vue'

const emit = defineEmits(['lifecycle'])
const text = ref('')

onMounted(() => {
  emit('lifecycle', { type: 'mounted', componentName: 'ComponentB' })
})

onUpdated(() => {
  emit('lifecycle', { type: 'updated', componentName: 'ComponentB' })
})

onBeforeUnmount(() => {
  emit('lifecycle', { type: 'beforeUnmount', componentName: 'ComponentB' })
})
</script>

Parent component usage:

<component :is="currentComponent" v-if="currentComponent" @lifecycle="handleLifecycle" />
const handleLifecycle = ({ type, componentName }) => {
  const statusMap = {
    mounted: '✅ Mounted',
    updated: '🔄 Updated',
    beforeUnmount: '❌ About to unmount'
  }
  componentStatus.value = `${componentName} ${statusMap[type]}`
  console.log(`${componentName} ${type}`)
}

Pros: stable, officially recommended. Cons: requires modifying each child component, leading to some repetitive code.

Solution 2: Access via ref (Specific Scenarios)

If you need to call methods on the component instance directly.

<component :is="currentComponent" v-if="currentComponent" ref="dynamicComponentRef" />
import { ref, watch, nextTick } from 'vue'

const dynamicComponentRef = ref(null)

watch(currentComponent, async (newComponent) => {
  if (newComponent) {
    await nextTick()
    console.log('Component instance:', dynamicComponentRef.value)
    componentStatus.value = '✅ Component mounted'
    if (dynamicComponentRef.value?.someMethod) {
      dynamicComponentRef.value.someMethod()
    }
  }
}, { immediate: true })

Pros: direct access to component methods and data. Cons: only captures the mount phase; updates and unmounts are not observed.

Solution 3: provide/inject (Deep Communication)

Suitable for complex nested component trees.

<!-- Parent -->
<script setup>
import { provide, ref } from 'vue'

const componentStatus = ref('No component')
const lifecycleHandler = {
  onMounted: (name) => {
    componentStatus.value = `✅ ${name} mounted`
    console.log(`${name} mounted`)
  },
  onUpdated: (name) => {
    componentStatus.value = `🔄 ${name} updated`
    console.log(`${name} updated`)
  },
  onBeforeUnmount: (name) => {
    componentStatus.value = `❌ ${name} about to unmount`
    console.log(`${name} about to unmount`)
  }
}
provide('lifecycleHandler', lifecycleHandler)
</script>

<template>
  <div>
    <div class="status">{{ componentStatus }}</div>
    <component :is="currentComponent" v-if="currentComponent" />
  </div>
</template>
<!-- Child -->
<script setup>
import { inject, onMounted, onUpdated, onBeforeUnmount } from 'vue'

const lifecycleHandler = inject('lifecycleHandler', {})
const componentName = 'ComponentA' // each component sets its own name

onMounted(() => lifecycleHandler.onMounted?.(componentName))
onUpdated(() => lifecycleHandler.onUpdated?.(componentName))
onBeforeUnmount(() => lifecycleHandler.onBeforeUnmount?.(componentName))
</script>

Pros: works across deep component hierarchies. Cons: adds boilerplate and requires explicit provision.

Comparison of Solutions

Emit events – easy to implement, highly reliable, best choice for most scenarios.

Ref access – moderate difficulty, good reliability, suitable when you need to call component methods.

provide/inject – higher difficulty, good reliability, ideal for deep nested communication.

@vue:mounted – minimal difficulty, low reliability, experimental use only, not recommended for production.

Conclusion

The code review shows that while the undocumented

@vue:[lifecycle]

syntax offers centralized management in dynamic component scenarios, its lack of documentation makes it unstable and risky for production. Prefer official patterns such as child‑emitted events, ref access, or provide/inject, and plan a migration away from undocumented APIs.

image.png
image.png
frontend developmentcode reviewVue.jsdynamic componentslifecycle hooksundocumented API
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.