skip to content

Mount & Unmount Phases

onBeforeMount, onMounted, onBeforeUnmount and onUnmounted fire in a fixed parent-child order and pair setup with cleanup. Interviewers ask where a fetch, DOM read or subscription goes.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In a Vue 3 `<script setup>` component that listens to window resize events, where do you add and remove the listener, and why?

level: juniorimportance: must knowfreq 75%

answer

  1. window outlives the component
  2. pair every acquire with a release
  3. same function reference both times
  4. onMounted and onUnmounted

basics

~20 s

Add the listener in onMounted and remove it in onUnmounted (or onBeforeUnmount), passing the same function reference. Vue does not clean up listeners you attach to window yourself, so skipping the removal leaks the handler and everything it closes over.

solid answer

~40 s

The `window` object outlives every component, and Vue only removes listeners it created from the template, so a listener added with `addEventListener` is yours to remove. The idiomatic pairing is `onMounted(() => window.addEventListener('resize', onResize))` and `onUnmounted(() => window.removeEventListener('resize', onResize))`, with `onResize` a named function so both calls get the **same reference**. `onMounted` is also where the DOM exists, so the handler can read element sizes on its first call. If the removal is missing, every mount adds another handler, each closure keeps the component's state reachable, and the handlers keep running after the component is gone. `onBeforeUnmount` works too when the cleanup still needs the component's DOM.

code

ts · 12 lines
ts
import { onMounted, onUnmounted } from 'vue'

export function useWindowResize(handler: () => void) {
  const controller = new AbortController()

  onMounted(() => {
    handler()
    window.addEventListener('resize', handler, { signal: controller.signal })
  })

  onUnmounted(() => controller.abort())
}

go deeper

for a junior

Recall the pairing: add in onMounted, remove in onUnmounted, with the same named function.

for a middle

Explain why Vue cannot clean up listeners on window, and when onBeforeUnmount is preferable to onUnmounted.

for a senior

Describe the leak symptoms, duplicate handlers and retained closures after navigation, and how an AbortController or a composable keeps acquire and release together.

for a principal

Push teams to package every external subscription in a composable that owns both halves, so a missed cleanup cannot be written at a call site.

## The scenario A `ResponsiveChart` component needs to redraw when the browser window changes size. The browser exposes this through `window.addEventListener('resize', handler)`. The question is where, in a Vue 3 `<script setup>` component, that call and its matching `removeEventListener` belong. ## The idiomatic answer ```ts import { ref, onMounted, onUnmounted } from 'vue' const width = ref(0) function onResize() { width.value = window.innerWidth } onMounted(() => { onResize() // initial value, DOM is ready window.addEventListener('resize', onResize) }) onUnmounted(() => { window.removeEventListener('resize', onResize) }) ``` Three details carry the answer: 1. **`onMounted` for the acquire.** The component's DOM now exists and is inserted, so the first call can measure elements through template refs. It is also the conventional home for browser-only side effects. 2. **`onUnmounted` for the release.** It runs after the component and its children are removed and after its render effect, computed values and watchers have been stopped. The Vue API reference names timers, DOM event listeners and server connections as exactly what this hook is for. 3. **The same function reference.** `removeEventListener` only removes a listener if it receives the identical function. An inline arrow passed to `addEventListener` can never be removed. ## Why Vue does not clean this up for you Vue tracks what it creates: listeners declared in the template with `@click` or `v-on`, reactive effects, watchers and computed values created during setup. A listener attached to `window` by your own code is invisible to it. The `window` object lives for the whole page, so it keeps a reference to your handler until you remove it. ## What goes wrong without the removal | Symptom | Cause | |---|---| | Handler runs after the component is gone | `window` still holds it | | Memory is not released after navigating away | the handler's closure keeps `width`, template refs and other state reachable | | Work doubles each time the component is shown again | every mount adds a new listener; none is removed | | Errors like reading a property of `null` | the handler touches a template ref that was cleared on unmount | After unmount the component's render effect is stopped, so writes from a stale handler no longer update the screen, which is why the leak can go unnoticed for a long time. ## onBeforeUnmount or onUnmounted? Both are correct places to remove a window listener: - **`onBeforeUnmount`** runs while the instance is still fully functional and its DOM is still in place. Choose it when the cleanup must read or detach something from the component's own elements, for example handing a DOM node back to a third-party library. - **`onUnmounted`** runs after the children are unmounted and the effects are stopped. It is the usual choice for pure resource release such as listeners, timers and sockets. The Options API equivalents are `mounted`, `beforeUnmount` and `unmounted`; the Vue 2 names `beforeDestroy` and `destroyed` were renamed in Vue 3. ## Variations worth knowing - **`AbortController`**: pass `{ signal: controller.signal }` to `addEventListener` and call `controller.abort()` in `onUnmounted`. One abort releases several listeners at once, and no function reference needs to be kept. - **Extract a composable**: a `useWindowSize()` function that registers the same `onMounted`/`onUnmounted` pair keeps the pairing in one place and makes it reusable across components. - **A component that unmounts before its mounted hooks run** (mounted and removed within the same tick) never runs its `onMounted` callbacks, but still runs its unmount hooks; removing a listener that was never added is harmless, so the pairing stays safe. ## Confirming a suspected leak When a leak like this is suspected in a running app, the check is mechanical: 1. Mount and unmount the component several times, for example by navigating to and away from its page. 2. Use the browser's developer tools to list the listeners attached to `window`: a growing number of `resize` entries means removals are missing. 3. Take a heap snapshot and search for the component's name or its state objects; detached instances that are still retained point at the closure that holds them. 4. Add the missing `onUnmounted` removal and repeat: the count should return to its baseline after every unmount. ## What interviewers listen for A clear statement that listeners on `window`, `document` or any object outside the component's template are the component's own responsibility, the named-function detail, and the choice between the two unmount hooks based on whether the DOM is still needed.

  • Why does removeEventListener sometimes fail to remove a listener added in onMounted?
    It removes only the exact function object that was added, with the same event type and capture setting. If `onMounted` passed an inline arrow such as `() => resize()`, `onUnmounted` creates a different arrow and nothing is removed. Store the handler in a named function or constant, or use an `AbortController` signal so removal does not depend on the reference.
  • When would you remove the listener in onBeforeUnmount instead of onUnmounted?
    When the cleanup still needs the component's own DOM or a fully working instance, for example calling a chart library's `destroy()` on the element it was attached to. `onBeforeUnmount` runs before anything is torn down; `onUnmounted` runs after children are unmounted and effects are stopped. For a plain window listener either works.
  • Does Vue remove listeners declared with @resize or v-on in the template?
    Yes. Listeners Vue attaches through the template are removed with the elements they belong to. The obligation applies to listeners your own code adds to objects Vue does not render, such as `window`, `document` or a media query list.

saying these in an interview costs you the question

  • Vue removes every event listener automatically on unmount
  • An inline arrow can be removed with another identical-looking arrow
  • The listener must be added in onBeforeMount to be ready in time
  • Stopping the render effect on unmount also detaches window listeners
  • Cleanup is only needed if the component is unmounted often
open as a page

In Vue 3, in what order do setup, onBeforeMount and onMounted run for a parent and its child, and how does unmounting order them?

level: middleimportance: must knowfreq 60%

basics

~10 s

Mounting goes parent setup, parent onBeforeMount, child setup, child onBeforeMount, child onMounted, then parent onMounted. Unmounting goes parent onBeforeUnmount, child onBeforeUnmount, child onUnmounted, then parent onUnmounted.

open as a page

In a Vue 3 `<script setup>` component, where should a data fetch start and where must a DOM measurement happen?

level: juniorimportance: should knowfreq 65%

basics

~20 s

A DOM measurement must wait for onMounted, because template refs are null until the component has rendered. A fetch needs no DOM, so it can start directly in setup, earlier than onMounted, as long as the component renders a loading state meanwhile.

open as a page

Why does Vue 3's Composition API have no onCreated hook, and which Options API hooks does `<script setup>` code replace?

level: middleimportance: should knowfreq 55%

basics

~20 s

setup() itself runs at the point where beforeCreate and created would, and in fact before both, so code written directly in <script setup> is the created phase. A separate onCreated hook would add nothing, so Vue does not provide one.

open as a page

A Vue 3 parent measures its children in onMounted, but one child created with defineAsyncComponent measures as empty: why, and how do you fix it?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

A parent's onMounted waits only for synchronous children. An async component mounts a lightweight wrapper immediately and the real component later, after its code loads, so at that moment there is only a placeholder. Measure when the child reports it has mounted, or observe its size.

open as a page