Vue.js 48 🟢 KeepAlive Specific Lifecycle Hooks (onActivated, onDeactivated)
A component inside a <KeepAlive> does not unmount when it is hidden. It is deactivated. The onUnmounted hook does not fire, the component instance is not destroyed, and the state is preserved. But the component is no longer visible, and the work it was doing—timers, subscriptions, polling, animations—should stop. The onActivated and onDeactivated hooks are the two lifecycle hooks that fire when a cached component becomes visible or hidden. They are the boundary between the active life of the component and its cached life, and they are the place where the pause-and-resume logic lives.
Key point: The onActivated hook fires both when the component is first mounted and every time it is reactivated from the cache. The onDeactivated hook fires every time the component is hidden. The onMounted hook fires only once, so any logic that must run each time the component appears belongs in onActivated, not onMounted.
Why these hooks exist
The refresh problem. A dashboard component fetches data on mount. Inside a cache, the onMounted hook fires only once, so the data is never refreshed. The user switches to another tab, the data changes, and when they return they see the stale snapshot. The onActivated hook fires on every show, which is where the refresh belongs.
The timer problem. A component that polls an API every five seconds starts the timer in onMounted and clears it in onUnmounted. Inside a cache, the onUnmounted hook does not fire, so the timer keeps running while the component is hidden. The onDeactivated hook is where the timer is cleared, and onActivated is where it restarts.
The subscription problem. A component subscribes to a WebSocket channel or an event emitter on mount. The subscription should be released when the component is hidden, or the hidden component continues to receive events. The onDeactivated hook is the place for the unsubscribe.
The animation problem. A component with a running animation, a carousel, or a video player should pause when it is hidden. The onDeactivated hook pauses it, and onActivated resumes it. Without these hooks, the animation runs off-screen, consuming CPU for content the user cannot see.
The scroll problem. The browser may reset the scroll position of the cached DOM when it is reinserted. The onActivated hook is where the scroll position is restored from a saved value.
The analytics problem. A component that reports a “page view” or a “tab view” event fires the report on every show, not just on mount. The onActivated hook is where the event fires.
The composition with the router problem. A <RouterView> wrapped in a <KeepAlive> caches the route components. Each route component that needs to refresh, pause, or restore on navigation uses the onActivated and onDeactivated hooks.
a. The onActivated hook
The onActivated hook fires when the component is inserted into the document. Inside a <KeepAlive>, it fires on the initial mount and on every subsequent activation.
The basic registration
<script setup>
import { onActivated } from 'vue';
onActivated(() => {
console.log('activated');
});
</script>
The hook is registered in the setup function. It takes no arguments and returns nothing.
The first mount
The onActivated hook fires after the onMounted hook on the initial render. Both run, but only onActivated fires on the later activations.
<script setup>
import { onMounted, onActivated } from 'vue';
onMounted(() => {
console.log('mounted'); // Fires once
});
onActivated(() => {
console.log('activated'); // Fires on mount and every show
});
</script>
The order on the first mount is onMounted, then onActivated.
The refresh pattern
The onActivated hook is the correct place to refresh data that may have changed while the component was cached.
<script setup>
import { ref, onActivated } from 'vue';
const data = ref(null);
async function fetchData() {
const response = await fetch('/api/dashboard');
data.value = await response.json();
}
onActivated(() => {
fetchData();
});
</script>
Every time the component is shown, the data is fetched fresh.
The timer restart
The onActivated hook restarts a timer that was cleared in onDeactivated.
<script setup>
import { ref, onActivated, onDeactivated } from 'vue';
let timer = null;
onActivated(() => {
timer = setInterval(() => {
console.log('tick');
}, 1000);
});
onDeactivated(() => {
clearInterval(timer);
timer = null;
});
</script>
The timer runs only while the component is visible.
The scroll restoration
The onActivated hook restores the scroll position of a cached list.
<script setup>
import { ref, onActivated, onDeactivated } from 'vue';
const listEl = ref(null);
let savedScroll = 0;
onDeactivated(() => {
savedScroll = listEl.value?.scrollTop ?? 0;
});
onActivated(() => {
if (listEl.value) {
listEl.value.scrollTop = savedScroll;
}
});
</script>
<template>
<div ref="listEl" class="list">
<!-- long list -->
</div>
</template>
The scroll position is saved when the component is hidden and restored when it is shown.
The analytics event
The onActivated hook fires a view event on every show.
<script setup>
import { onActivated } from 'vue';
onActivated(() => {
analytics.track('tab_viewed', { tab: 'dashboard' });
});
</script>
The event fires once per activation, not once per mount.
The form refresh
A component that displays server-side data refreshes it on every activation.
<script setup>
import { ref, onActivated } from 'vue';
const items = ref([]);
onActivated(async () => {
const response = await fetch('/api/items');
items.value = await response.json();
});
</script>
The form is populated with the latest data whenever the user returns to it.
b. The onDeactivated hook
The onDeactivated hook fires when the component is removed from the document. Inside a <KeepAlive>, it fires every time the component is hidden, and the component is cached rather than destroyed.
The basic registration
<script setup>
import { onDeactivated } from 'vue';
onDeactivated(() => {
console.log('deactivated');
});
</script>
The hook fires when the component becomes inactive.
The cleanup pattern
The onDeactivated hook is the correct place to release resources that should not run while the component is hidden.
<script setup>
import { onDeactivated } from 'vue';
let subscription = null;
onDeactivated(() => {
subscription?.unsubscribe();
subscription = null;
});
</script>
The subscription is released on every hide, and the onActivated hook re-establishes it on the next show.
The timer cleanup
The onDeactivated hook clears the timer that onActivated starts.
<script setup>
import { onActivated, onDeactivated } from 'vue';
let timer = null;
onActivated(() => {
timer = setInterval(poll, 5000);
});
onDeactivated(() => {
clearInterval(timer);
timer = null;
});
</script>
The two hooks form a pair: start on activate, stop on deactivate.
The video pause
A video player pauses when the component is hidden.
<script setup>
import { ref, onActivated, onDeactivated } from 'vue';
const videoEl = ref(null);
onDeactivated(() => {
videoEl.value?.pause();
});
onActivated(() => {
videoEl.value?.play();
});
</script>
<template>
<video ref="videoEl" src="..."></video>
</template>
The video does not play off-screen.
The animation pause
A component with a running animation pauses it when hidden.
<script setup>
import { onActivated, onDeactivated } from 'vue';
import { gsap } from 'gsap';
let animation = null;
onActivated(() => {
animation?.resume();
});
onDeactivated(() => {
animation?.pause();
});
</script>
The animation is paused when the component is not visible.
The state save
The onDeactivated hook saves the component’s scroll position, cursor position, or draft before it is hidden.
<script setup>
import { ref, onDeactivated } from 'vue';
const scrollTop = ref(0);
const editorEl = ref(null);
onDeactivated(() => {
scrollTop.value = editorEl.value?.scrollTop ?? 0;
});
</script>
The saved value is restored in onActivated.
The debug log
A debugging pattern logs each activation and deactivation with a timestamp.
<script setup>
import { onActivated, onDeactivated } from 'vue';
onActivated(() => {
console.log('[tab] activated at', Date.now());
});
onDeactivated(() => {
console.log('[tab] deactivated at', Date.now());
});
</script>
The log shows the activation history of the component.
c. The lifecycle interactions
The hooks interact with the other lifecycle hooks in specific ways. The order is deterministic, and understanding it prevents subtle bugs.
The full order
The complete order of the lifecycle hooks for a cached component is:
1. onMounted (once, on first creation)
2. onActivated (on first show)
3. onDeactivated (on first hide)
4. onActivated (on second show)
5. onDeactivated (on second hide)
6. ...
7. onUnmounted (on eviction or parent unmount)
The onMounted hook fires once, the onActivated and onDeactivated hooks alternate, and the onUnmounted hook fires once at the end.
The onMounted versus onActivated
The onMounted hook is for one-time setup: registering event listeners that persist, initializing a library, or computing a value that does not change. The onActivated hook is for logic that must run on every show: refreshing data, restarting timers, or restoring the scroll position.
<script setup>
import { onMounted, onActivated } from 'vue';
onMounted(() => {
// One-time setup: initialize the library.
initLibrary();
});
onActivated(() => {
// Every show: refresh the data.
refreshData();
});
</script>
The onUnmounted versus onDeactivated
The onUnmounted hook is for final cleanup: releasing resources that are no longer needed, closing connections, or saving the final state. The onDeactivated hook is for pausing: stopping the timers and releasing the subscriptions that onActivated re-establishes.
<script setup>
import { onUnmounted, onDeactivated } from 'vue';
onUnmounted(() => {
// Final cleanup: close the connection.
connection.close();
});
onDeactivated(() => {
// Pause: stop the polling.
clearInterval(timer);
});
</script>
The sibling order
When multiple cached components are inside the same <KeepAlive>, the hooks fire in the order the components are rendered. The deactivation of the old component fires before the activation of the new one when the transition mode is out-in.
The transition interaction
With <Transition mode="out-in">, the deactivation of the old component fires before the activation of the new one. With the default mode, both fire in the same tick.
The async component interaction
An async component inside a <KeepAlive> has its onActivated hook fire after the async setup resolves. The first activation waits for the component to load.
The nested component interaction
A parent and a child component both inside a <KeepAlive> each have their own hooks. The parent’s onActivated fires before the child’s on the initial mount.
The hook table
| Hook | First Mount | Subsequent Shows | Hide | Eviction |
|---|---|---|---|---|
onMounted | Yes | No | No | No |
onActivated | Yes | Yes | No | No |
onDeactivated | No | No | Yes | No |
onUnmounted | No | No | No | Yes |
Complete Example Session
<!-- ============================================
PART 1: THE BASIC HOOKS
============================================ -->
<script setup>
import { onActivated, onDeactivated } from 'vue';
onActivated(() => console.log('activated'));
onDeactivated(() => console.log('deactivated'));
</script>
<!-- ============================================
PART 2: THE ORDER
============================================ -->
<script setup>
import { onMounted, onActivated, onDeactivated, onUnmounted } from 'vue';
onMounted(() => console.log('mounted'));
onActivated(() => console.log('activated'));
onDeactivated(() => console.log('deactivated'));
onUnmounted(() => console.log('unmounted'));
</script>
<!-- Output on first show:
mounted
activated
Output on hide:
deactivated
Output on second show:
activated
Output on eviction:
deactivated
unmounted
-->
<!-- ============================================
PART 3: THE DATA REFRESH
============================================ -->
<script setup>
import { ref, onActivated } from 'vue';
const data = ref(null);
async function fetchData() {
const response = await fetch('/api/dashboard');
data.value = await response.json();
}
onActivated(() => {
fetchData();
});
</script>
<!-- ============================================
PART 4: THE TIMER PAIR
============================================ -->
<script setup>
import { onActivated, onDeactivated } from 'vue';
let timer = null;
onActivated(() => {
timer = setInterval(() => console.log('tick'), 1000);
});
onDeactivated(() => {
clearInterval(timer);
timer = null;
});
</script>
<!-- ============================================
PART 5: THE SUBSCRIPTION PAIR
============================================ -->
<script setup>
import { onActivated, onDeactivated } from 'vue';
let unsubscribe = null;
onActivated(() => {
unsubscribe = store.subscribe((state) => {
// handle state
});
});
onDeactivated(() => {
unsubscribe?.();
unsubscribe = null;
});
</script>
<!-- ============================================
PART 6: THE SCROLL PRESERVATION
============================================ -->
<script setup>
import { ref, onActivated, onDeactivated } from 'vue';
const listEl = ref(null);
let savedScroll = 0;
onDeactivated(() => {
savedScroll = listEl.value?.scrollTop ?? 0;
});
onActivated(() => {
if (listEl.value) {
listEl.value.scrollTop = savedScroll;
}
});
</script>
<template>
<div ref="listEl" class="list">...</div>
</template>
<!-- ============================================
PART 7: THE VIDEO PAUSE
============================================ -->
<script setup>
import { ref, onActivated, onDeactivated } from 'vue';
const videoEl = ref(null);
onActivated(() => videoEl.value?.play());
onDeactivated(() => videoEl.value?.pause());
</script>
<template>
<video ref="videoEl" src="..."></video>
</template>
<!-- ============================================
PART 8: THE ANALYTICS EVENT
============================================ -->
<script setup>
import { onActivated } from 'vue';
onActivated(() => {
analytics.track('view', { tab: 'reports' });
});
</script>
<!-- ============================================
PART 9: THE DEBUG LOG
============================================ -->
<script setup>
import { onActivated, onDeactivated } from 'vue';
onActivated(() => {
console.log('[tab] activated at', new Date().toISOString());
});
onDeactivated(() => {
console.log('[tab] deactivated at', new Date().toISOString());
});
</script>
<!-- ============================================
PART 10: THE COMPLETE PATTERN
============================================ -->
<!--
1. onActivated fires on every show, including the first mount.
2. onDeactivated fires on every hide.
3. Use onActivated for refresh, restart, and restore.
4. Use onDeactivated for pause, stop, and save.
5. Use onMounted for one-time setup.
6. Use onUnmounted for final cleanup.
7. The hooks form pairs: start and stop.
8. The hooks compose with Transition, Suspense, and the router.
-->
The ten parts showed the basic hooks, the order, the data refresh, the timer pair, the subscription pair, the scroll preservation, the video pause, the analytics event, the debug log, and the complete pattern.
Quick Reference
The Hooks
| Hook | Fires When |
|---|---|
onActivated | The component becomes visible |
onDeactivated | The component is hidden |
The Hook Pairs
| Start | Stop |
|---|---|
onActivated starts a timer | onDeactivated clears it |
onActivated subscribes | onDeactivated unsubscribes |
onActivated resumes animation | onDeactivated pauses it |
onActivated refreshes data | onDeactivated saves state |
onActivated plays video | onDeactivated pauses it |
The Lifecycle Order
| Event | Hooks |
|---|---|
| First mount | onMounted, onActivated |
| Hide | onDeactivated |
| Show again | onActivated |
| Eviction | onDeactivated, onUnmounted |
The onMounted vs onActivated
| Hook | Fires |
|---|---|
onMounted | Once, on creation |
onActivated | On mount and every show |
The onUnmounted vs onDeactivated
| Hook | Fires |
|---|---|
onUnmounted | Once, on destruction |
onDeactivated | Every time the component is hidden |
Best Practices
✅ Do This:
<!-- Use onActivated for refresh logic. -->
onActivated(() => {
fetchData();
}); <!-- ✅ -->
<!-- Use onDeactivated for pause logic. -->
onDeactivated(() => {
clearInterval(timer);
}); <!-- ✅ -->
<!-- Pair the start and stop logic. -->
onActivated(() => { timer = setInterval(poll, 1000); });
onDeactivated(() => { clearInterval(timer); }); <!-- ✅ -->
<!-- Use onMounted for one-time setup. -->
onMounted(() => {
initLibrary();
}); <!-- ✅ -->
<!-- Save and restore the scroll position. -->
onDeactivated(() => { savedScroll = el.value.scrollTop; });
onActivated(() => { el.value.scrollTop = savedScroll; }); <!-- ✅ -->
<!-- Use the hooks with the router. -->
// Inside a cached route component. <!-- ✅ -->
❌ Don’t Do This:
<!-- Don't use onMounted for refresh logic. -->
onMounted(() => {
fetchData(); <!-- ❌ Fires only once -->
}); <!-- ❌ -->
<!-- Don't leave the timer running when hidden. -->
onMounted(() => {
setInterval(poll, 1000); <!-- ❌ Runs off-screen -->
}); <!-- ❌ -->
<!-- Don't forget to release the subscription. -->
onMounted(() => {
store.subscribe(handler); <!-- ❌ Never released -->
}); <!-- ❌ -->
<!-- Don't rely on onUnmounted for pause logic. -->
onUnmounted(() => {
clearInterval(timer); <!-- ❌ Does not fire on hide -->
}); <!-- ❌ -->
<!-- Don't forget to restart the timer on activation. -->
onDeactivated(() => {
clearInterval(timer); <!-- ❌ Never restarted -->
}); <!-- ❌ -->
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Data not refreshed | Used onMounted | Use onActivated |
| Timer runs off-screen | No onDeactivated | Clear it on deactivate |
| Subscription leaked | No unsubscribe | Release on deactivate |
| Scroll reset | Not saved | Save and restore |
| Video plays hidden | No pause | Pause on deactivate |
| Animation runs hidden | No pause | Pause on deactivate |
| Analytics fires once | Used onMounted | Use onActivated |
Real-World Examples
1. Data Refresh
onActivated(() => fetchData());
2. Timer Pair
onActivated(() => { timer = setInterval(poll, 1000); });
onDeactivated(() => clearInterval(timer));
3. Subscription Pair
onActivated(() => { unsubscribe = store.subscribe(handler); });
onDeactivated(() => unsubscribe());
4. Scroll Preservation
onDeactivated(() => { savedScroll = el.value.scrollTop; });
onActivated(() => { el.value.scrollTop = savedScroll; });
5. Video Pause
onActivated(() => video.play());
onDeactivated(() => video.pause());
6. Analytics
onActivated(() => analytics.track('view', { tab: 'reports' }));
7. The Order
onMounted(() => console.log('mounted'));
onActivated(() => console.log('activated'));
onDeactivated(() => console.log('deactivated'));
8. The Debug Log
onActivated(() => console.log('activated at', Date.now()));
9. The Animation Pause
onActivated(() => animation.resume());
onDeactivated(() => animation.pause());
10. The Complete Pair
onActivated(() => { start(); });
onDeactivated(() => { stop(); });
Visual
The Lifecycle Order
┌────────────────────────────────────────────────────────────┐
│ THE LIFECYCLE ORDER │
│ │
│ FIRST MOUNT │
│ ┌──────────────────────────────────────────────────┐ │
│ │ onMounted │ │
│ │ ↓ │ │
│ │ onActivated │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ HIDE │
│ ┌──────────────────────────────────────────────────┐ │
│ │ onDeactivated │ │
│ │ (no onUnmounted) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ SHOW AGAIN │
│ ┌──────────────────────────────────────────────────┐ │
│ │ onActivated │ │
│ │ (no onMounted) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ EVICTION │
│ ┌──────────────────────────────────────────────────┐ │
│ │ onDeactivated │ │
│ │ ↓ │ │
│ │ onUnmounted │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Hook Pairs
┌────────────────────────────────────────────────────────────┐
│ THE HOOK PAIRS │
│ │
│ onActivated onDeactivated │
│ ─────────── ───────────── │
│ start timer → clear timer │
│ subscribe → unsubscribe │
│ resume anim → pause anim │
│ play video → pause video │
│ refresh data → save state │
│ restore scroll → save scroll │
│ track view → (nothing) │
│ │
│ Each start pairs with a stop. │
└────────────────────────────────────────────────────────────┘
The Data Refresh
┌────────────────────────────────────────────────────────────┐
│ THE DATA REFRESH PATTERN │
│ │
│ WITHOUT onActivated │
│ ┌──────────────────────────────────────────────────┐ │
│ │ First mount: │ │
│ │ onMounted → fetchData() │ │
│ │ The data is loaded. │ │
│ │ │ │
│ │ Switch to another tab and back: │ │
│ │ The component is activated. │ │
│ │ onMounted does not fire. │ │
│ │ The data is stale. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ WITH onActivated │
│ ┌──────────────────────────────────────────────────┐ │
│ │ First mount: │ │
│ │ onActivated → fetchData() │ │
│ │ The data is loaded. │ │
│ │ │ │
│ │ Switch to another tab and back: │ │
│ │ onActivated → fetchData() │ │
│ │ The data is refreshed. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Router Integration
┌────────────────────────────────────────────────────────────┐
│ THE ROUTER INTEGRATION │
│ │
│ <RouterView v-slot="{ Component }"> │
│ <KeepAlive> │
│ <component :is="Component" /> │
│ </KeepAlive> │
│ </RouterView> │
│ │
│ Each route component uses: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ onActivated(() => { │ │
│ │ // Refresh the data on navigation. │ │
│ │ }); │ │
│ │ │ │
│ │ onDeactivated(() => { │ │
│ │ // Pause the timers and subscriptions. │ │
│ │ }); │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The state is preserved across navigations. │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
onActivated | Fires when the component becomes visible |
onDeactivated | Fires when the component is hidden |
| First mount | onMounted, then onActivated |
| Hide | onDeactivated |
| Show again | onActivated |
| Eviction | onDeactivated, onUnmounted |
Use onActivated for | Refresh, restart, restore |
Use onDeactivated for | Pause, stop, save |
Use onMounted for | One-time setup |
Use onUnmounted for | Final cleanup |
Key takeaways:
onActivatedfires on every show, including the first mount. TheonMountedhook fires only once, so any logic that must run each time the component appears belongs inonActivated. The two hooks fire in order on the first mount.onDeactivatedfires on every hide. Inside a<KeepAlive>, the component is not unmounted when it is hidden. TheonDeactivatedhook is where the pause logic belongs.- The hooks form pairs. The
onActivatedhook starts a timer, and theonDeactivatedhook clears it. TheonActivatedhook subscribes, and theonDeactivatedhook unsubscribes. The pairing is the pattern. - The data refresh belongs in
onActivated. A dashboard that fetches data on mount shows a stale snapshot when the user returns. TheonActivatedhook refreshes the data on every show. - The scroll position must be saved and restored explicitly. The browser may reset the scroll position of the cached DOM. The
onDeactivatedhook saves the value, and theonActivatedhook restores it. - The
onUnmountedhook is for final cleanup. It fires on eviction or parent unmount, not on hide. TheonDeactivatedhook is for the pause-and-resume logic. - The hooks compose with the router. A
<RouterView>wrapped in a<KeepAlive>caches the route components, and each route component uses the hooks for the refresh and pause logic. - The hooks fire in a deterministic order. The
onMountedhook fires first, thenonActivated. TheonDeactivatedhook fires on hide, and theonUnmountedhook fires on eviction. The order prevents the subtle bugs.
Remember: The onActivated and onDeactivated hooks are the boundary between the active life and the cached life of a component. The onActivated hook fires every time the component becomes visible, and the onDeactivated hook fires every time it is hidden. They are the place for the refresh, restart, and restore logic on activation, and the pause, stop, and save logic on deactivation. The onMounted hook fires only once, so it belongs to the one-time setup, and the onUnmounted hook belongs to the final cleanup. The result is a cached component that refreshes its data, pauses its timers, preserves its scroll position, and reports its views correctly on every show, and a tabbed interface, a wizard, or a router that preserves the state without leaking the resources.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!