In a Vue 3 component cached by `<KeepAlive>`, when exactly do `onActivated` and `onDeactivated` run, and what work belongs in them?
answer
- two extra lifecycle states
- first mount counts as activation
- final unmount counts as deactivation
- descendants receive them too
basics
~20 sIn Vue 3, onActivated runs on the cached component's first mount and every time it is reinserted; onDeactivated runs each time it is moved into the cache and on final unmount. Use them for show/hide work: refresh, pause, resume.
solid answer
~40 sA component under `<KeepAlive>` has two extra states. `onActivated` fires on the **initial mount** (right after `onMounted`) and every time the instance is reinserted from the cache; `onDeactivated` fires every time it is moved into the cache and also when it is finally unmounted. Both apply to the cached root **and all its descendants**, which can register them too. Because `setup()` and `onMounted` run only once for a cached instance, anything that must happen on each visit belongs in these hooks: refreshing data, restarting a poll or a `requestAnimationFrame` loop, re-adding a global listener; its counterpart, stopping them, belongs in `onDeactivated`. Putting the same fetch in both `onMounted` and `onActivated` runs it twice on the first visit. In the Options API the hooks are `activated` and `deactivated`.
code
vue · 23 lines<script setup lang="ts">
import { ref, onActivated, onDeactivated } from 'vue'
const rates = ref<number[]>([])
let timer: ReturnType<typeof setInterval> | undefined
async function load() {
rates.value = await fetch('/api/rates').then((r) => r.json())
}
onActivated(() => {
load()
timer = setInterval(load, 30_000)
})
onDeactivated(() => {
clearInterval(timer)
timer = undefined
})
</script>
<template>
<ul><li v-for="r in rates" :key="r">{{ r }}</li></ul>
</template>go deeper
Remember the pair: onActivated when a cached component is shown, onDeactivated when it is hidden into the cache.
State the edge timings precisely: onActivated also on first mount, onDeactivated also on final unmount, and both for every descendant in the cached tree.
Place per-visit work correctly: refresh and resume in onActivated, pause and remove listeners in onDeactivated, never duplicated in onMounted.
Encapsulate show/hide behaviour in composables that use the activation hooks when cached, so feature code behaves the same whether or not a view is kept alive.
## Two extra lifecycle states A normal component is mounted once and unmounted once. A component cached by `<KeepAlive>` can be **deactivated** (taken out of the page and parked in the cache) and **activated** (put back) many times in between. Vue exposes these transitions as two Composition API hooks imported from `vue`: `onActivated()` and `onDeactivated()`. The Options API equivalents are the `activated` and `deactivated` options. ## When each hook fires | Moment | `onMounted` | `onActivated` | `onDeactivated` | `onUnmounted` | |---|---|---|---|---| | First render inside `<KeepAlive>` | Yes | Yes, right after | No | No | | Switched away, instance cached | No | No | Yes | No | | Switched back from cache | No | Yes | No | No | | Finally destroyed (KeepAlive unmounts, or entry evicted) | No | No | Yes when it was the active one | Yes | Two rows surprise people: 1. **`onActivated` also fires on the initial mount.** It is not "only on return". That is by design: it lets you write the per-visit work once. 2. **`onDeactivated` also fires on unmount** of the currently active cached component, so cleanup placed there runs even when the whole `<KeepAlive>` goes away. ## Descendants get the hooks too The hooks are not limited to the component that is the direct child of `<KeepAlive>`. Every component **inside the cached tree** can call `onActivated`/`onDeactivated`, and they fire when the cached root is activated or deactivated. A chart deep inside a cached dashboard tab can therefore resize itself when the tab is shown, without the tab component having to forward anything. ## What belongs in them Because `setup()` runs once per instance, anything that must happen on **every** visit cannot live in `setup()` or `onMounted`. Typical pairs: - **Refresh on show**: refetch or revalidate data that may be stale after the component sat in the cache. - **Pause and resume background work**: clear a `setInterval` poll, cancel a `requestAnimationFrame` loop or stop a websocket subscription in `onDeactivated`, and restart it in `onActivated`. A deactivated instance is still alive; its timers and watchers keep running unless you stop them. - **Global listeners**: add `window` or `document` listeners (keyboard shortcuts, `resize`) in `onActivated` and remove them in `onDeactivated`, so a hidden tab does not react to key presses meant for the visible one. - **Focus and scroll**: move focus to the first field or restore a scroll position of a window-level scroller when the view is shown again. ```vue <script setup lang="ts"> import { onActivated, onDeactivated } from 'vue' function onKey(e: KeyboardEvent) { if (e.key === 's' && e.ctrlKey) { e.preventDefault(); /* save draft */ } } onActivated(() => window.addEventListener('keydown', onKey)) onDeactivated(() => window.removeEventListener('keydown', onKey)) </script> ``` ## Mistakes to avoid - **Duplicated first-visit work.** Fetching in both `onMounted` and `onActivated` runs the fetch twice on the first visit, since `onActivated` also fires after the first mount. Keep per-visit work in `onActivated` alone. - **Cleanup only in `onUnmounted`.** For a cached component `onUnmounted` may not run for a long time; a listener added on each activation and removed only on unmount piles up duplicates. - **Expecting the hooks outside `<KeepAlive>`.** In a component that is never cached, `onActivated`/`onDeactivated` never fire; they are specific to the cached tree. - **Treating `onActivated` as a re-render signal.** It fires when the instance is reinserted, not when props or state change; normal reactivity still drives re-renders. ## Activation hooks versus mount hooks | Question | Mount hooks | Activation hooks | |---|---|---| | How often per instance? | Once each | Every show and hide | | Fire outside `<KeepAlive>`? | Yes | No | | Good for | One-time setup, creating third-party widgets | Per-visit refresh, pause/resume, global listeners | | Cleanup partner | `onUnmounted` | `onDeactivated` | A useful habit: write the component so that it would also work without `<KeepAlive>`. One-time work stays in `setup()`/`onMounted` with cleanup in `onUnmounted`; per-visit work goes in `onActivated`/`onDeactivated`. The same component then behaves correctly whether a parent decides to cache it or not. ## Summary `onActivated` = shown (including the first time); `onDeactivated` = hidden (including the last time). Put per-visit work and its reverse in this pair, keep one-time setup in `setup()`, and remember every descendant in the cached tree can listen too.
- Why does data fetched in both `onMounted` and `onActivated` load twice on the first visit?Because `onActivated` also fires on the initial mount of a cached component, right after `onMounted`. Both hooks run on the first visit, so the fetch runs twice. Put per-visit loading in `onActivated` only; it covers the first visit and every return.
- A chart three levels below the cached tab needs to resize when the tab is shown; how?Call `onActivated` in the chart component itself. The activation hooks fire for every component in the cached tree, not just the direct child of `<KeepAlive>`, so the chart can resize on activation without the tab forwarding an event.
saying these in an interview costs you the question
- onActivated only fires when returning, never on the first mount
- Only the direct child of KeepAlive can use onActivated
- onDeactivated never runs when the component is finally unmounted
- onActivated fires whenever the cached component's props change
- Cleanup in onUnmounted is enough for listeners added on each activation