Files
agent-skills/skills/vue-best-practices/reference/transition-unmount-hook-timing.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

150 lines
4.2 KiB
Markdown

---
title: Unmount Hooks May Not Fire Inside Transitions During Fast Replacement
impact: MEDIUM
impactDescription: Components inside transitions can be destroyed without unmount hooks firing under race conditions
type: gotcha
tags: [vue3, lifecycle, transition, onUnmounted, unmounted, cleanup, race-condition]
---
# Unmount Hooks May Not Fire Inside Transitions During Fast Replacement
**Impact: MEDIUM** - When a component inside a `<transition>` is replaced by another component during the transition's loading phase, the unmount hooks (`onBeforeUnmount`, `onUnmounted`) may not be called even though the component is removed from the DOM. This can cause memory leaks and resource leaks from unclean side effects.
This is a known edge case that occurs when the timing is specific - if a parent component with a child inside a transition is replaced while the child is still mounting. The child's mount hooks fire, but unmount hooks never do.
## Task Checklist
- [ ] Be aware that unmount hooks are not 100% guaranteed inside transitions
- [ ] For critical cleanup, consider alternative cleanup strategies
- [ ] Use `mode="out-in"` on transitions to ensure old component fully unmounts before new mounts
- [ ] For essential resources, consider cleanup at parent component level
- [ ] Test component replacement scenarios during development
**Problematic Scenario:**
```vue
<!-- Parent component with lazy-loaded child in transition -->
<template>
<transition>
<Suspense>
<component :is="currentComponent" />
</Suspense>
</transition>
</template>
```
```javascript
// Child component - unmount hooks may not fire if parent changes quickly
export default {
setup() {
const socket = new WebSocket('wss://example.com')
onMounted(() => {
console.log('Mounted - this will run')
socket.connect()
})
onUnmounted(() => {
// WARNING: This may NOT run if component is inside transition
// and parent navigates away during mounting phase!
console.log('Unmounted - might not run')
socket.close()
})
}
}
```
**Safer Patterns:**
```vue
<!-- SAFER: Use out-in mode to ensure proper sequencing -->
<template>
<transition mode="out-in">
<component :is="currentComponent" :key="currentKey" />
</transition>
</template>
```
```javascript
// SAFER: Cleanup at parent level for critical resources
// Parent component
export default {
setup() {
const childSocket = ref(null)
// Parent controls resource lifecycle
provide('registerSocket', (socket) => {
childSocket.value = socket
})
onUnmounted(() => {
// Parent ensures cleanup even if child unmount hook doesn't fire
childSocket.value?.close()
})
}
}
// Child component
export default {
setup() {
const registerSocket = inject('registerSocket')
const socket = new WebSocket('wss://example.com')
// Register with parent for backup cleanup
registerSocket(socket)
onMounted(() => {
socket.connect()
})
onUnmounted(() => {
socket.close() // Still attempt cleanup here
})
}
}
```
```javascript
// SAFER: Use AbortController pattern for cancellable operations
export default {
setup() {
const abortController = new AbortController()
onMounted(() => {
fetch('/api/data', { signal: abortController.signal })
.then(handleData)
.catch(err => {
if (err.name !== 'AbortError') {
handleError(err)
}
})
})
onUnmounted(() => {
// If this doesn't fire, request continues but response is ignored
// Not a memory leak - just potentially wasted network call
abortController.abort()
})
}
}
```
## Testing for This Issue
```javascript
// Test by rapidly switching components during async loading
async function testUnmountHooks() {
// Mount component A (has async setup)
await mountComponent('A')
// Immediately switch to B before A finishes mounting
await mountComponent('B')
// Check if A's unmount hooks fired
// They may not have!
}
```
## Reference
- [Vue.js GitHub Issue #6260](https://github.com/vuejs/core/issues/6260)
- [Vue.js Transition](https://vuejs.org/guide/built-ins/transition.html)
- [Vue.js Lifecycle Hooks](https://vuejs.org/guide/essentials/lifecycle.html)