| |

Vue.js 48 🟢 KeepAlive Specific Lifecycle Hooks (onActivated, onDeactivated)

⏱️ Estimated read: 17 min | |

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

HookFirst MountSubsequent ShowsHideEviction
onMountedYesNoNoNo
onActivatedYesYesNoNo
onDeactivatedNoNoYesNo
onUnmountedNoNoNoYes

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

HookFires When
onActivatedThe component becomes visible
onDeactivatedThe component is hidden

The Hook Pairs

StartStop
onActivated starts a timeronDeactivated clears it
onActivated subscribesonDeactivated unsubscribes
onActivated resumes animationonDeactivated pauses it
onActivated refreshes dataonDeactivated saves state
onActivated plays videoonDeactivated pauses it

The Lifecycle Order

EventHooks
First mountonMounted, onActivated
HideonDeactivated
Show againonActivated
EvictiononDeactivated, onUnmounted

The onMounted vs onActivated

HookFires
onMountedOnce, on creation
onActivatedOn mount and every show

The onUnmounted vs onDeactivated

HookFires
onUnmountedOnce, on destruction
onDeactivatedEvery 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

PitfallWhy It HappensFix
Data not refreshedUsed onMountedUse onActivated
Timer runs off-screenNo onDeactivatedClear it on deactivate
Subscription leakedNo unsubscribeRelease on deactivate
Scroll resetNot savedSave and restore
Video plays hiddenNo pausePause on deactivate
Animation runs hiddenNo pausePause on deactivate
Analytics fires onceUsed onMountedUse 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

ItemValue
onActivatedFires when the component becomes visible
onDeactivatedFires when the component is hidden
First mountonMounted, then onActivated
HideonDeactivated
Show againonActivated
EvictiononDeactivated, onUnmounted
Use onActivated forRefresh, restart, restore
Use onDeactivated forPause, stop, save
Use onMounted forOne-time setup
Use onUnmounted forFinal cleanup

Key takeaways:

  • onActivated fires on every show, including the first mount. The onMounted hook fires only once, so any logic that must run each time the component appears belongs in onActivated. The two hooks fire in order on the first mount.
  • onDeactivated fires on every hide. Inside a <KeepAlive>, the component is not unmounted when it is hidden. The onDeactivated hook is where the pause logic belongs.
  • The hooks form pairs. The onActivated hook starts a timer, and the onDeactivated hook clears it. The onActivated hook subscribes, and the onDeactivated hook 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. The onActivated hook 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 onDeactivated hook saves the value, and the onActivated hook restores it.
  • The onUnmounted hook is for final cleanup. It fires on eviction or parent unmount, not on hide. The onDeactivated hook 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 onMounted hook fires first, then onActivated. The onDeactivated hook fires on hide, and the onUnmounted hook 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.


Was this helpful?


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!