In Vue 3, how does onScopeDispose() differ from onUnmounted(), and what does getCurrentScope() let you check before calling it?
answer
- one needs an instance, one doesn't
- tied to the active scope
- works inside a manual scope
- undefined outside any scope
basics
~20 sonUnmounted() needs an active component instance; onScopeDispose() needs only an active effect scope, so it works in a component and in a manual effectScope(). getCurrentScope() returns that scope or undefined, letting code register cleanup only when one exists.
solid answer
~40 s`onUnmounted()` is a lifecycle hook: it attaches to the current component instance and warns if called outside setup. `onScopeDispose()` attaches its callback to the current **effect scope** and runs it when that scope's `stop()` is called. Because every component's setup runs inside the component's scope, it covers the component case — firing during unmount, after `onBeforeUnmount` and before `onUnmounted` — and also covers code running inside a hand-made `effectScope().run()` where no instance exists. `getCurrentScope()` returns the active `EffectScope` or `undefined`, so a reusable function can write `if (getCurrentScope()) onScopeDispose(cleanup)`. Without a scope, `onScopeDispose()` registers nothing and warns in development; since 3.5 a second argument `true` silences that warning.
code
ts · 18 linesimport { getCurrentScope, onScopeDispose, effectScope, ref } from 'vue'
export function startClock(ms = 1000) {
const now = ref(Date.now())
const id = setInterval(() => (now.value = Date.now()), ms)
const stop = () => clearInterval(id)
// register cleanup only when an owner scope exists
if (getCurrentScope()) {
onScopeDispose(stop)
}
return { now, stop }
}
// works outside any component, where onUnmounted would warn
const scope = effectScope()
scope.run(() => startClock())
scope.stop() // interval cleared by the onScopeDispose callbackgo deeper
Recall that onScopeDispose is the cleanup hook that works both in components and in a manual effect scope, while onUnmounted needs a component.
Explain that the component's setup runs inside its effect scope, so onScopeDispose fires at unmount, and that getCurrentScope() returns undefined when no scope is active.
Show how a guard on getCurrentScope() plus a returned stop function keeps a resource releasable in every call context, and why the silent flag is not a fix.
Set the convention for shared code: scope-based cleanup over lifecycle hooks, and an explicit owner for anything called outside a scope.
## Two ways to register teardown Code that acquires something — an event listener, a timer, a socket, a subscription to an external store — has to release it. Vue 3 offers two registration points that look similar and are anchored to different owners. - **`onUnmounted(cb)`** is a **lifecycle hook**. It registers `cb` on the *current component instance*, which exists only while that component's `setup()` is running. Called anywhere else it registers nothing and warns in development that it 'is called when there is no active component instance to be associated with'. - **`onScopeDispose(cb)`** registers `cb` on the *active effect scope*. It runs when that scope's `stop()` is called. ## Why onScopeDispose covers the component case Every component instance owns an effect scope, and Vue turns it on while setup runs. So inside setup, the active scope *is* the component's scope, and `onScopeDispose(cb)` behaves like an unmount hook. The timing differs slightly from `onUnmounted`: 1. Vue runs the component's `onBeforeUnmount` hooks; 2. it calls the component scope's `stop()` — collected watchers stop, then `onScopeDispose` callbacks run; 3. it unmounts the rendered subtree; 4. the `onUnmounted` hooks run, queued to fire after that. For releasing a resource the difference rarely matters, but it means a scope-dispose callback cannot assume the DOM has already been removed. ## Where only onScopeDispose works When code runs inside `effectScope().run()` outside any component — an app-level service, a feature started from a plugin, a test — there is no component instance, so `onUnmounted` has nothing to attach to. `onScopeDispose` attaches to the manual scope and fires when your code calls `scope.stop()`. That makes it the portable choice for any function that might be called from either context, and it is why the API reference calls it a non-component-coupled replacement for `onUnmounted`. | | `onUnmounted()` | `onScopeDispose()` | |---|---|---| | Attaches to | current component instance | active effect scope | | Works in a component's setup | yes | yes (the component's scope) | | Works in `effectScope().run()` outside components | no, warns | yes | | Fires | after the subtree is unmounted | when the scope's `stop()` runs | | Relative order in a component | after the scope stops | before `onUnmounted` | | No owner available | warns, nothing registered | warns, nothing registered; `true` as 2nd arg silences it (3.5+) | ## getCurrentScope(): checking before registering `getCurrentScope()` returns the active `EffectScope`, or `undefined` when none is active. Its main use is a guard in a function that may run with or without an owner: - inside a component's setup it returns the component's scope; - inside `effectScope().run()` it returns that scope, even though `getCurrentInstance()` would return `null`; - at module level, in a timer, or in a promise continuation after setup, it returns `undefined`. With the guard, the function registers cleanup when an owner exists and otherwise leaves release to its caller — for example by also returning a `stop` function. The 3.5 `failSilently` argument (`onScopeDispose(cb, true)`) only hides the warning; the callback is still **not registered** when no scope is active, so it is not a substitute for thinking about who stops the resource. ## What the scope does not tell you `getCurrentScope()` says nothing about *whose* scope it is. A detached scope, a nested scope and a component scope all look the same to the caller, and the cleanup fires whenever that particular scope stops. If a function is called inside a detached scope, its cleanup waits for that detached scope's explicit `stop()` — not for the component that happened to be rendering. ## What interviewers probe - 'Why does my cleanup not run?' — the function was called after setup returned, so no scope was active and nothing was registered. - 'Can I use `onUnmounted` in code that also runs outside components?' — no; use `onScopeDispose`. - 'Does the silent flag make it safe?' — it removes noise, not the leak.
- In Vue 3, does passing true as the second argument to onScopeDispose() make it safe to call anywhere?No. The 3.5 `failSilently` argument only suppresses the development warning. With no active scope the callback is still not registered, so the resource is never released unless the caller stops it some other way, for example through a returned `stop` function.
- Inside a component's setup, when does an onScopeDispose() callback run compared with onBeforeUnmount and onUnmounted?Between them. On unmount Vue runs the `onBeforeUnmount` hooks, then stops the component's scope — stopping its watchers and running its `onScopeDispose` callbacks — then unmounts the subtree, and the `onUnmounted` hooks run after that.
saying these in an interview costs you the question
- onScopeDispose only works inside components, like any lifecycle hook
- onUnmounted works in an effectScope().run() outside any component
- Passing true to onScopeDispose registers the callback with no scope
- getCurrentScope() and getCurrentInstance() always return the same owner
- onScopeDispose callbacks run after onUnmounted hooks