Chapter 107: Vue ‘deactivated’ Lifecycle Hook
Deactivated
(and its modern Composition API name onDeactivated)
This hook has nothing to do with the normal lifecycle of a component that is simply shown/hidden with v-if / v-show.
It exists exclusively when a component is wrapped inside <KeepAlive>.
Together with its sibling activated / onActivated, these two hooks are the “alive” part of KeepAlive.
1. Mental model – When & why deactivated exists
Normal behavior (without <KeepAlive>):
|
0 1 2 3 4 5 6 7 |
v-if="false" → onBeforeUnmount → onUnmounted v-if="true" → brand new instance → onBeforeMount → onMounted |
→ Every time the component disappears and reappears → new instance, state lost, timers restarted, expensive re-init, scroll position reset, form values cleared, etc.
With <KeepAlive>:
|
0 1 2 3 4 5 6 7 |
v-if="false" → onDeactivated (component is cached → instance stays alive, DOM removed temporarily) v-if="true" → onActivated (cached instance reused, DOM re-inserted) |
→ The component never really dies — it keeps its:
- internal state (ref values, reactive objects)
- watchers
- timers / intervals
- event listeners
- third-party instances (charts, maps, video players…)
- scroll positions
- form inputs values
- etc.
deactivated = “I’m being hidden / cached → time to pause / save / stop temporary things”
activated = “I’m being shown again → time to resume / restore / restart”
2. Real situations where you need deactivated / onDeactivated
| Situation | What happens without deactivated? | What you usually do in deactivated / activated |
|---|---|---|
| Tab content / router-view inside <KeepAlive> | Every tab switch → full re-mount → state lost, API called again, scroll reset | deactivated → pause polling/WebSocket/video, save form draft, save scroll position activated → resume polling, restore scroll, refocus input |
| Drawer / sidebar / off-canvas menu | Close drawer → re-opening creates new instance | deactivated → pause animations, stop audio previews activated → restart animation, refocus first input |
| Modal inside <KeepAlive> | Re-opening modal → new instance → focus lost, form cleared | deactivated → save draft, pause video activated → restore form, refocus first field |
| Chat window / infinite scroll feed | Switch route/tab → scroll position lost | deactivated → save scrollTop activated → restore scrollTop |
| Video / audio player inside cached tab | Switch tab → video stops / restarts from 0 | deactivated → pause playback, save currentTime activated → resume playback from saved time |
| Real-time data feed / WebSocket | Switch away → connection closed unnecessarily | deactivated → pause / unsubscribe temporarily activated → reconnect / resume subscription |
3. Real, Practical Example – Tabbed Interface with Preserved State & Scroll
App.vue (wraps tabs in <KeepAlive>)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
<template> <div class="tabs-app"> <div class="tab-buttons"> <button v-for="tab in tabs" :key="tab.name" :class="{ active: activeTab === tab.name }" @click="activeTab = tab.name" > {{ tab.label }} </button> </div> <!-- KeepAlive → components stay alive when switched --> <KeepAlive> <component :is="currentTabComponent" /> </KeepAlive> </div> </template> <script setup lang="ts"> import { ref, computed } from 'vue' import TabHome from './TabHome.vue' import TabChat from './TabChat.vue' import TabProfile from './TabProfile.vue' const activeTab = ref('home') const tabs = [ { name: 'home', label: 'Home', component: TabHome }, { name: 'chat', label: 'Chat', component: TabChat }, { name: 'profile', label: 'Profile', component: TabProfile } ] const currentTabComponent = computed(() => { return tabs.find(t => t.name === activeTab.value)?.component }) </script> <style scoped> .tabs-app { max-width: 900px; margin: 3rem auto; } .tab-buttons { display: flex; gap: 0.5rem; margin-bottom: 2rem; } .tab-buttons button { padding: 0.8rem 1.5rem; border: 1px solid #ccc; border-radius: 6px; background: white; cursor: pointer; } .tab-buttons .active { background: #3b82f6; color: white; border-color: #3b82f6; } </style> |
TabChat.vue – shows onActivated / onDeactivated in action
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
<template> <div class="tab-chat"> <h2>Chat Tab – state & scroll preserved</h2> <div ref="chatContainer" class="chat-window"> <div v-for="msg in messages" :key="msg.id" class="message" > {{ msg.text }} </div> </div> <div class="input-row"> <input v-model="newMessage" @keyup.enter="sendMessage" placeholder="Type a message..." /> <button @click="sendMessage">Send</button> </div> <p>Messages count: {{ messages.length }}</p> </div> </template> <script setup lang="ts"> import { ref, onActivated, onDeactivated, onMounted, nextTick } from 'vue' const messages = ref([ { id: 1, text: 'Hello everyone!' }, { id: 2, text: 'How is the project going?' }, // ... imagine 50+ messages ]) const newMessage = ref('') const chatContainer = ref<HTMLElement | null>(null) const savedScrollTop = ref(0) onMounted(() => { console.log('Chat – mounted (only first time)') }) onActivated(async () => { console.log('Chat – activated (tab visible again)') await nextTick() if (chatContainer.value) { // Restore previous scroll position chatContainer.value.scrollTop = savedScrollTop.value // Optional: scroll to bottom if user was at bottom if (savedScrollTop.value + chatContainer.value.clientHeight >= chatContainer.value.scrollHeight - 10) { chatContainer.value.scrollTop = chatContainer.value.scrollHeight } } }) onDeactivated(() => { console.log('Chat – deactivated (tab hidden)') if (chatContainer.value) { // Save current scroll position savedScrollTop.value = chatContainer.value.scrollTop } }) function sendMessage() { if (!newMessage.value.trim()) return messages.value.push({ id: Date.now(), text: newMessage.value.trim() }) newMessage.value = '' // Auto-scroll to bottom after sending nextTick(() => { if (chatContainer.value) { chatContainer.value.scrollTop = chatContainer.value.scrollHeight } }) } </script> <style scoped> .tab-chat { padding: 2rem; background: white; border-radius: 12px; } .chat-window { height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; } .message { margin: 0.5rem 0; padding: 0.8rem; background: #f0f0f0; border-radius: 8px; max-width: 80%; } .input-row { display: flex; gap: 0.8rem; } input { flex: 1; padding: 0.9rem; border: 1px solid #d1d5db; border-radius: 6px; } button { padding: 0.9rem 1.6rem; background: #3b82f6; color: white; border: none; border-radius: 6px; cursor: pointer; } </style> |
4. Quick Summary Table – activated / deactivated in 2026
| Question | activated / onActivated | deactivated / onDeactivated |
|---|---|---|
| When does it run? | When cached component becomes visible again | When cached component is hidden / cached |
| Does it run on first mount? | Yes (right after mounted) | No (only when hidden first time) |
| Does component re-mount? | No – cached instance reused | No – instance kept alive |
| State / watchers / timers? | All preserved | All preserved (until you pause them) |
| DOM / refs available? | Yes – DOM re-inserted | Yes – DOM still there (but hidden) |
| Most common use | Restore scroll, refocus input, resume polling/video/animation | Save scroll, pause polling/video, blur input, save draft |
| Do modern developers use it? | Very often – in tabs, drawers, modals with <KeepAlive> | Very often – same scenarios |
Pro Tips from Real Projects (Hyderabad 2026)
- Most common pattern → <KeepAlive> + tabs / router-view + onActivated / onDeactivated
- Use onActivated to resume things: refocus input, restart polling/WebSocket, resume video, restore scroll
- Use onDeactivated to pause things: pause video, stop polling, save draft, blur input
- Always save/restore scroll position in tabs / infinite lists / chat feeds inside <KeepAlive>
- In Nuxt / Vue Router → onActivated / onDeactivated are essential for keeping page state when reusing components
- Combine with nextTick() in onActivated when restoring scroll / focus (DOM re-insertion timing can vary)
- Never do one-time initialization in onActivated — use onMounted for that
Your mini homework:
- Create the tabbed interface above with <KeepAlive>
- Add long scrollable content + textarea in one tab
- Switch tabs multiple times → verify textarea content & scroll position are preserved
- Add console.log in onActivated / onDeactivated → observe timing
Any part confusing? Want full examples for:
- onActivated + scroll restoration in infinite chat / feed?
- onDeactivated + pause/resume video player inside cached tab?
- <KeepAlive> + router-view + activated pattern in Vue Router / Nuxt?
- activated / deactivated vs mounted / unmounted comparison in <KeepAlive>?
Just tell me — we’ll build the next preserved-state tab / drawer / chat system together step by step 🚀
