skip to content

In a Vue 3 <script setup> component, what happens when onMounted is registered twice, once directly and once inside a composable?

level: juniorimportance: should knowfreq 42%

answer

  1. hooks append, never replace
  2. registration order is run order
  3. composable hooks join the caller
  4. setup hooks before options hooks

basics

~10 s

Vue 3 keeps a list per hook on the component instance, so both onMounted callbacks run, in the order they were registered during setup. A composable's hook attaches to whichever component called the composable.

solid answer

~40 s

Each call to `onMounted` appends a callback to that hook's list on the component instance that is active while `setup()` runs; nothing replaces anything. So a direct `onMounted` and one registered inside a composable both run, in the order the calls happened during setup. A composable has no instance of its own: its hooks attach to the component that called it, and calling the same composable twice registers its hooks twice. Passing the very same function to `onMounted` twice also runs it twice, because each registration is wrapped separately. If the component also has an Options API `mounted()` option, that runs after the hooks registered in `setup()`, because Vue applies options after `setup()` returns. This is what lets several composables each own their own setup and cleanup without knowing about each other.

code

ts · 14 lines
ts
// useWindowWidth.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useWindowWidth() {
  const width = ref(0)
  const update = () => { width.value = window.innerWidth }
  onMounted(() => {
    update()
    window.addEventListener('resize', update)
    console.log('b: composable mounted')
  })
  onUnmounted(() => window.removeEventListener('resize', update))
  return width
}

go deeper

for a junior

Recall that onMounted can be called several times, all callbacks run in registration order, and a composable's hooks belong to the component that called it.

for a middle

Explain the per-instance hook lists, the current-instance mechanism that lets a composable in another file register hooks, and why setup hooks precede options hooks.

for a senior

Use accumulation deliberately: give each composable its own paired setup and cleanup, and avoid duplicate teardown in the component that uses it.

for a principal

Treat self-cleaning composables as the unit of reuse, and review that every hook a composable registers has a matching cleanup in the same composable.

## Registering a lifecycle hook In Vue 3's **Composition API**, lifecycle hooks are functions you call during component setup: `onMounted`, `onUpdated`, `onBeforeUnmount`, `onUnmounted` and others, imported from `vue`. Calling `onMounted(cb)` does not run `cb`; it **registers** it. Vue stores the callback on the **component instance** that is currently being set up, in a list kept for that hook type, and calls every callback in the list when the component reaches that phase. The instance is found implicitly. While a component's `setup()` — or the body of `<script setup>` — runs, Vue records that component as the **current instance**. Every hook registration during that synchronous run is attached to it. ## Several registrations of one hook Because each call appends to a list: 1. `onMounted(a)` adds `a`. 2. A composable called next that does `onMounted(b)` adds `b`. 3. `onMounted(c)` adds `c`. After mount, Vue runs `a`, `b`, `c` in that order. Nothing is overwritten, and there is no limit on how many callbacks one hook may carry. | Situation | Result | |---|---| | Two `onMounted` calls in one component | Both run, in registration order | | A composable registers `onMounted` | Runs as part of the calling component's mount | | The same composable called twice | Its hooks are registered twice and run twice | | The same function passed to `onMounted` twice | Runs twice; each registration is wrapped separately | | `setup()` hook plus an Options API `mounted()` option | `setup()`-registered hooks first, then the option | ## Why composables depend on this A **composable** is a function such as `useWindowSize()` that bundles state with its own setup and teardown. It can call `onMounted` to attach a listener and `onUnmounted` to remove it, and the component that uses it never sees those details. This works only because: - the hooks attach to the **caller's** instance — whichever component is being set up when the composable runs; - hooks **accumulate**, so three composables can each register `onMounted` and `onUnmounted` without interfering with one another or with the component's own hooks; - each call gets its own closure, so two uses of `useWindowSize()` in one component keep separate listeners and separate cleanups. The composable does not need to be written inside the component. The Vue docs are explicit that the call need not be lexically inside `setup()`; it only has to happen on a **synchronous call stack** that starts in `setup()`. ## Mixing styles in one component An options component may also declare `setup()`. Vue calls `setup()` first and applies the options object afterwards, so hooks registered in `setup()` are already in the list when the `mounted` option is added. The practical effect: in a component with both, `setup()`-registered `onMounted` callbacks run before the options `mounted()`. ## The same rule on the way out Teardown hooks follow the same list semantics. `onBeforeUnmount` and `onUnmounted` callbacks from the component and from every composable it used all run when the component is removed, in the order they were registered. That gives a simple discipline for composables: pair every resource with its release **inside the same composable** — `addEventListener` in `onMounted`, `removeEventListener` in `onUnmounted` — so the component can add or remove the composable without touching any other hook. Two composables that each register teardown never need to coordinate; each cleans up only what it created. ## Where this goes wrong - **Assuming the last registration wins** and deleting a "duplicate" hook that a composable depended on. - **Assuming a composable's hooks are global**, and registering cleanup a second time in the component, so a listener is removed twice or a counter is decremented twice. - **Registering outside the synchronous setup run** — in a `setTimeout`, a `.then()` or after an `await` — where no instance is current, so the hook attaches to nothing. ## How to answer Say that hooks are lists on the instance, that registration order is run order, and that composables' hooks belong to the component that called them. Mention that options hooks run after `setup()` ones in a mixed component, and that all of this depends on registering synchronously during setup.

  • Can a Vue 3 composable be defined in another file and still register hooks for the component?
    Yes. The hook attaches to the component whose setup is running when `onMounted` is called, not to the file or function that contains the call. The Vue docs state the call need not be lexically inside `setup()`, as long as it happens on a synchronous call stack that starts in `setup()` or `<script setup>`.
  • In a Vue 3 component with both setup() and an Options API mounted() option, which runs first on mount?
    The hooks registered in `setup()`. Vue runs `setup()` first and applies the options object afterwards, so the `mounted` option is appended after any `onMounted` callbacks already in the instance's list.

saying these in an interview costs you the question

  • A second onMounted call replaces the first one.
  • A composable's onMounted runs once globally, not per component.
  • Hooks registered inside a composable run before the component's own hooks.
  • Passing the same function to onMounted twice runs it only once.
  • The Options API mounted() option always runs before setup()'s onMounted.