skip to content

In Vue 3.5, what do watch() and watchEffect() return, and when would you stop, pause or resume a watcher yourself?

level: juniorimportance: should knowfreq 45%

answer

  1. a callable handle
  2. setup watchers stop on unmount
  3. stop is final, pause is not
  4. changes during a pause

basics

~20 s

They return a WatchHandle: call it, or its stop(), to end the watcher for good, and since Vue 3.5 use pause() and resume() to suspend it. Watchers created synchronously in setup stop automatically on unmount, so manual stops are for other cases.

solid answer

~40 s

`watch()` and `watchEffect()` return a handle. Calling it, or `handle.stop()`, stops the watcher permanently: it unsubscribes and runs its registered cleanups. Since Vue 3.5 the handle also has `pause()` and `resume()`: while paused, triggers are remembered but nothing runs; on `resume()` the watcher is re-triggered once, and a `watch` callback fires only if the source value differs from the last one it saw. You rarely stop setup watchers by hand, because a watcher created synchronously in `setup()` or `<script setup>` is bound to the component and stopped when it unmounts. You stop manually when the watcher was created asynchronously, outside a component, or when it should end earlier - for example a one-off 'wait until ready' watcher. Pausing suits temporarily ignoring changes, such as while a panel is hidden.

code

ts · 13 lines
ts
import { ref, watch } from 'vue'

const query = ref('a')
const handle = watch(query, (next, prev) => {
  console.log(prev, '->', next)
})

handle.pause()
query.value = 'b'
query.value = 'c'
handle.resume() // after the flush: logs "a -> c" once

handle.stop()   // permanent; runs cleanups, never fires again

go deeper

for a junior

Know that watch() and watchEffect() return a handle you call to stop them, and that setup-created watchers stop on unmount automatically.

for a middle

Explain stop versus pause and resume, including that changes during a pause cause at most one re-run and that pause does not run cleanups.

for a senior

Recognise which watchers are not bound to a component and own their handles, and choose pause, stop or once for the lifecycle each watcher actually needs.

for a principal

Prefer designs where watcher lifetimes follow component or scope lifetimes, so manual handles stay the rare, reviewed exception.

## What the call returns In Vue 3, `watch()` and `watchEffect()` (and `watchPostEffect()` / `watchSyncEffect()`) all return a **`WatchHandle`**: ```ts const handle = watch(source, callback) handle() // stop - the handle itself is callable handle.stop() // the same function handle.pause() // 3.5+ handle.resume() // 3.5+ ``` Before 3.5 the return value was only a stop function, which is why older code names it `unwatch` or `stop`. Destructuring works too: `const { stop, pause, resume } = watchEffect(...)`. ## When Vue stops a watcher for you Watchers created **synchronously** inside `setup()` or `<script setup>` are collected by the component's effect scope. When the component unmounts, the scope is stopped, and every watcher in it is stopped with it. For the ordinary case, you never touch the handle. A watcher is **not** stopped for you when: - it was created in an asynchronous callback (a timer, a promise continuation, after an `await` in a hook), where no component scope is active; - it was created in module scope or a plain function called outside any component; - it should end **before** the component unmounts. ## `stop` - permanent Stopping a watcher: 1. unsubscribes it from everything it tracked, so it never runs again; 2. runs the cleanups registered in its last run (with `onCleanup` or `onWatcherCleanup`); 3. removes it from the scope that collected it. There is no restart; to watch again, create a new watcher. A typical early stop is a one-shot wait: ```ts const stop = watch(isReady, (ready) => { if (ready) { start() stop() } }) ``` (Vue 3.4 added the `once: true` option for the simplest version of this.) ## `pause` and `resume` - temporary (3.5+) `pause()` keeps the watcher's subscriptions but stops it from running: - while paused, a change to something it depends on is **remembered**, not executed; - `pause()` does **not** run cleanups - the last run's resources stay alive; - `resume()` checks whether anything was triggered during the pause and, if so, re-triggers the watcher **once**. For `watch`, that single re-run then compares the current source value with the last value the callback saw. If `query` went `'a'` to `'b'` to `'c'` while paused, the callback runs once with `('c', 'a')`. If it went `'a'` to `'b'` and back to `'a'`, the callback does not run at all. Pausing is **not** a replay buffer. Good uses: ignoring changes while a tab or panel is hidden, suspending an expensive sync while the user is dragging, holding off a save while a bulk edit is applied. ## Choosing | Need | Use | |---|---| | Watcher lives as long as the component | nothing - created in setup, stopped on unmount | | Watcher should run once and end | `once: true`, or call the handle in the callback | | Watcher created asynchronously | keep the handle and stop it yourself, or create it synchronously with a condition | | Ignore changes for a while, then catch up | `pause()` / `resume()` | | Tear down many watchers together | an effect scope, rather than many handles | ## Handles outside components Watchers created in module scope, in a plain function called at application start, or in a test have no component to follow. Nothing stops them unless you do, so the handle is the only teardown you have: - a module-level watcher that syncs state to storage lives for the page's lifetime, which is often intended; - a watcher created per request, per test or per dialog opening must be stopped when that unit of work ends, or one more copy accumulates every time; - when a function creates a watcher on behalf of a caller, return the handle (or a dispose function) so the caller can own the lifetime. ## Pitfalls - Calling a stop handle inside a `watchEffect` during its **first** run can fail if the handle variable is not assigned yet, because `watchEffect` runs immediately; `watch` without `immediate` does not have this problem. - Pausing does not free resources; if the watcher holds a socket or a timer, stopping (or cleaning up) is the right tool. - A paused watcher inside a component is still stopped on unmount.

  • In Vue 3.5, does pausing a watcher run the cleanup it registered?
    No. `pause()` only suspends execution; the cleanup registered in the last run stays pending and runs when the watcher next re-runs or is stopped. If the watcher holds a live resource such as a socket or timer that must be released while paused, pausing is the wrong tool - stop it or release the resource explicitly.
  • Why is stopping a watcher inside the first run of watchEffect() risky in Vue 3?
    `watchEffect()` runs its function immediately, before the call returns the handle. Code such as `const stop = watchEffect(() => { if (done.value) stop() })` would hit an unassigned `stop` if the condition is already true on that first run. `watch()` without `immediate` defers the first callback, so the handle exists by then; `once: true` avoids the issue entirely.

saying these in an interview costs you the question

  • Every watcher must be stopped manually in onUnmounted.
  • pause() and resume() replay each change made during the pause.
  • A stopped watcher can be restarted by calling resume().
  • pause() runs the watcher's cleanup, releasing its resources.
  • watch() returns the callback's return value.