Skip to content

Conversation

9romise
Copy link

@9romise 9romise commented Sep 9, 2025

This PR adds support for a new option signal to the watch API in @vue/reactivity.

It accepts an AbortSignal. When provided, the watcher will be stopped once the corresponding AbortController is aborted.

This provides a more flexible way to stop multiple watchers at once by sharing a single AbortController.

Before:

const count = ref(0)

const cb1 = () => {}
const cb2 = () => {}

const stop1 = watch(count, cb1)
const stop2 = watch(count, cb2)

stop1()
stop2()

After:

const count = ref(0)
const controller = new AbortController()

const cb1 = () => {}
const cb2 = () => {}

watch(count, cb1, { signal: controller.signal })
watch(count, cb2, { signal: controller.signal })

controller.abort()

Summary by CodeRabbit

  • New Features

    • Added AbortController-based cancellation for reactive watchers. You can pass a signal to cancel one or multiple watchers at once. Works across effect and source-based watchers, including post and sync variants. Existing behavior remains unchanged unless a signal is provided.
  • Tests

    • Added tests verifying that aborting a shared signal stops all associated watchers and that no further updates occur after cancellation.

Copy link

coderabbitai bot commented Sep 9, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds AbortSignal-based cancellation to reactivity watchers. The watch API now accepts an optional signal to stop a watcher on abort. Runtime-core exposes a BaseWatchEffectOptions with signal and updates watchPostEffect/watchSyncEffect signatures. Tests verify abort-driven cancellation for watch, watchEffect, and multiple watches sharing one signal.

Changes

Cohort / File(s) Summary
Reactivity: AbortSignal support in watch
packages/reactivity/src/watch.ts
Adds signal?: AbortSignal to WatchOptions; registers abort listener to stop the watch handle when signaled. Existing behavior unchanged when no signal provided.
Reactivity tests: watch abort behavior
packages/reactivity/__tests__/watch.spec.ts
Adds test ensuring two watches sharing one AbortSignal are both stopped after abort.
Runtime-core: options surface for watch effects
packages/runtime-core/src/apiWatch.ts
Introduces BaseWatchEffectOptions with optional signal; WatchEffectOptions extends it; updates watchPostEffect and watchSyncEffect signatures to accept BaseWatchEffectOptions.
Runtime-core tests: aborting watchers/effects
packages/runtime-core/__tests__/apiWatch.spec.ts
Adds tests verifying AbortController stops watchEffect and watch watchers from reacting after abort.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Caller
  participant AC as AbortController
  participant W as watch()
  participant E as Reactive Effect
  participant H as WatchHandle

  C->>AC: const { signal } = new AbortController()
  C->>W: watch(source, cb, { signal })
  W->>E: create reactive effect
  W-->>H: return handle (stop)

  Note over E,H: Normal operation
  E-->>C: on source change -> invoke cb

  Note over AC,H: Abort path (new)
  C->>AC: controller.abort()
  AC-->>W: signal "abort" event
  W->>H: H.stop()
  H-->>E: teardown effect (unsubscribe)

  Note over E: After abort: no further cb invocations
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the primary change, namely adding support for AbortController to the watch API in the reactivity package, and follows the conventional commit format with scope and type.
Description Check ✅ Passed The description clearly outlines the new signal option for the watch API, explains its behavior with AbortController, and provides before-and-after code examples that directly relate to the changeset.

Poem

I twitch my ears at signals’ call,
A whisper: “Abort!”—and I stop, that’s all.
Two watches nap beneath one tree,
One puff of wind, both wander free.
I thump the ground—tests pass, of course! 🐇
Now onward hop, with lighter force.

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the primary change, namely adding support for AbortController to the watch API in the reactivity package, and follows the conventional commit format with scope and type.
Description Check ✅ Passed The description clearly outlines the new signal option for the watch API, explains its behavior with AbortController, and provides before-and-after code examples that directly relate to the changeset.

Poem

I twitch my ears at signals’ call,
A whisper: “Abort!”—and I stop, that’s all.
Two watches nap beneath one tree,
One puff of wind, both wander free.
I thump the ground—tests pass, of course! 🐇
Now onward hop, with lighter force.

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the primary change, namely adding support for AbortController to the watch API in the reactivity package, and follows the conventional commit format with scope and type.
Description Check ✅ Passed The description clearly outlines the new signal option for the watch API, explains its behavior with AbortController, and provides before-and-after code examples that directly relate to the changeset.

Poem

I twitch my ears at signals’ call,
A whisper: “Abort!”—and I stop, that’s all.
Two watches nap beneath one tree,
One puff of wind, both wander free.
I thump the ground—tests pass, of course! 🐇
Now onward hop, with lighter force.

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the primary change, namely adding support for AbortController to the watch API in the reactivity package, and follows the conventional commit format with scope and type.
Description Check ✅ Passed The description clearly outlines the new signal option for the watch API, explains its behavior with AbortController, and provides before-and-after code examples that directly relate to the changeset.

Poem

I twitch my ears at signals’ call,
A whisper: “Abort!”—and I stop, that’s all.
Two watches nap beneath one tree,
One puff of wind, both wander free.
I thump the ground—tests pass, of course! 🐇
Now onward hop, with lighter force.

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the primary change, namely adding support for AbortController to the watch API in the reactivity package, and follows the conventional commit format with scope and type.
Description Check ✅ Passed The description clearly outlines the new signal option for the watch API, explains its behavior with AbortController, and provides before-and-after code examples that directly relate to the changeset.

Poem

I twitch my ears at signals’ call,
A whisper: “Abort!”—and I stop, that’s all.
Two watches nap beneath one tree,
One puff of wind, both wander free.
I thump the ground—tests pass, of course! 🐇
Now onward hop, with lighter force.

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the primary change, namely adding support for AbortController to the watch API in the reactivity package, and follows the conventional commit format with scope and type.
Description Check ✅ Passed The description clearly outlines the new signal option for the watch API, explains its behavior with AbortController, and provides before-and-after code examples that directly relate to the changeset.

Poem

I twitch my ears at signals’ call,
A whisper: “Abort!”—and I stop, that’s all.
Two watches nap beneath one tree,
One puff of wind, both wander free.
I thump the ground—tests pass, of course! 🐇
Now onward hop, with lighter force.

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the primary change, namely adding support for AbortController to the watch API in the reactivity package, and follows the conventional commit format with scope and type.
Description Check ✅ Passed The description clearly outlines the new signal option for the watch API, explains its behavior with AbortController, and provides before-and-after code examples that directly relate to the changeset.

Poem

I twitch my ears at signals’ call,
A whisper: “Abort!”—and I stop, that’s all.
Two watches nap beneath one tree,
One puff of wind, both wander free.
I thump the ground—tests pass, of course! 🐇
Now onward hop, with lighter force.

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the primary change, namely adding support for AbortController to the watch API in the reactivity package, and follows the conventional commit format with scope and type.
Description Check ✅ Passed The description clearly outlines the new signal option for the watch API, explains its behavior with AbortController, and provides before-and-after code examples that directly relate to the changeset.

Poem

I twitch my ears at signals’ call,
A whisper: “Abort!”—and I stop, that’s all.
Two watches nap beneath one tree,
One puff of wind, both wander free.
I thump the ground—tests pass, of course! 🐇
Now onward hop, with lighter force.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed The description clearly outlines the new signal option for the watch API, explains its behavior with AbortController, and provides before-and-after code examples that directly relate to the changeset.
Title Check ✅ Passed The title "feat(watch): add support for AbortController" accurately and concisely summarizes the primary change — adding AbortSignal/AbortController support to the watch API — and aligns with the PR objectives and the code and test updates that introduce a signal option and abort handling. It is specific, short, and free of extraneous noise.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
packages/reactivity/src/watch.ts (1)

126-126: Handle pre-aborted signals and remove abort listener on manual stop

  • If signal.aborted is already true, stop immediately.
  • When stop() is called manually, remove the abort listener to avoid retaining references.

Apply:

   const watchHandle: WatchHandle = () => {
-    effect.stop()
+    // detach abort listener if present
+    if (signal && 'removeEventListener' in signal) {
+      signal.removeEventListener('abort', watchHandle)
+    }
+    effect.stop()
     if (scope && scope.active) {
       remove(scope.effects, effect)
     }
   }

   if (signal) {
-    signal.addEventListener('abort', watchHandle, { once: true })
+    if (signal.aborted) {
+      watchHandle()
+    } else {
+      signal.addEventListener('abort', watchHandle, { once: true })
+    }
   }

Also applies to: 214-221, 230-233

packages/reactivity/__tests__/watch.spec.ts (1)

293-312: Add a test for pre-aborted signals

Verify that a watcher with an already-aborted signal never runs and detaches immediately. This guards the new fast-path.

Example:

 it('stop multiple watches by abort controller', async () => {
   ...
 })
+
+it('pre-aborted signal stops watch immediately', async () => {
+  const controller = new AbortController()
+  controller.abort()
+  const state = ref(0)
+  const cb = vi.fn()
+  watch(state, cb, { signal: controller.signal, immediate: true })
+  await nextTick()
+  expect(cb).not.toHaveBeenCalled()
+  state.value++
+  await nextTick()
+  expect(cb).not.toHaveBeenCalled()
+})
packages/runtime-core/__tests__/apiWatch.spec.ts (1)

435-475: Good coverage for abort-driven stop

Both effect and source variants are validated. Consider mirroring the pre-aborted case here as well for completeness, but not required.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75220c7 and 0d5dc3e.

📒 Files selected for processing (4)
  • packages/reactivity/__tests__/watch.spec.ts (1 hunks)
  • packages/reactivity/src/watch.ts (3 hunks)
  • packages/runtime-core/__tests__/apiWatch.spec.ts (1 hunks)
  • packages/runtime-core/src/apiWatch.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
packages/reactivity/__tests__/watch.spec.ts (3)
packages/reactivity/src/watch.ts (1)
  • watch (121-334)
packages/runtime-core/src/apiWatch.ts (1)
  • watch (130-143)
packages/runtime-core/src/scheduler.ts (1)
  • nextTick (61-67)
packages/runtime-core/src/apiWatch.ts (2)
packages/reactivity/src/effect.ts (1)
  • DebuggerOptions (23-26)
packages/runtime-core/src/index.ts (2)
  • DebuggerOptions (222-222)
  • WatchEffectOptions (232-232)
packages/runtime-core/__tests__/apiWatch.spec.ts (2)
packages/runtime-core/src/apiWatch.ts (2)
  • watchEffect (59-64)
  • watch (130-143)
packages/reactivity/src/watch.ts (1)
  • watch (121-334)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules
  • GitHub Check: Header rules
  • GitHub Check: Pages changed
🔇 Additional comments (1)
packages/runtime-core/src/apiWatch.ts (1)

68-69: LGTM — signatures widened correctly

watchPostEffect/watchSyncEffect now accept the same base options (including signal) without exposing flush in their types. This matches usage and keeps the API tight.

Also applies to: 79-80

Copy link

github-actions bot commented Sep 9, 2025

Size Report

Bundles

File Size Gzip Brotli
compiler-dom.global.prod.js 84 kB 29.8 kB 26.3 kB
runtime-dom.global.prod.js 104 kB (+71 B) 39.2 kB (+41 B) 35.3 kB (+19 B)
vue.global.prod.js 162 kB (+71 B) 59.3 kB (+40 B) 52.9 kB (+14 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 47.2 kB 18.4 kB 16.9 kB
createApp 56.1 kB (+71 B) 21.6 kB (+30 B) 19.7 kB (+26 B)
createApp + vaporInteropPlugin 68.7 kB (+71 B) 25.9 kB (+31 B) 23.7 kB (+62 B)
createVaporApp 20.9 kB 8.26 kB 7.57 kB
createSSRApp 60.4 kB (+71 B) 23.4 kB (+27 B) 21.3 kB (+32 B)
defineCustomElement 61.1 kB (+71 B) 23.2 kB (+29 B) 21.1 kB (+33 B)
overall 70.5 kB (+71 B) 26.9 kB (+43 B) 24.5 kB (+51 B)

Copy link

pkg-pr-new bot commented Sep 9, 2025

Open in StackBlitz

@vue/compiler-core

npm i https://pkg.pr.new/@vue/compiler-core@13861

@vue/compiler-dom

npm i https://pkg.pr.new/@vue/compiler-dom@13861

@vue/compiler-sfc

npm i https://pkg.pr.new/@vue/compiler-sfc@13861

@vue/compiler-ssr

npm i https://pkg.pr.new/@vue/compiler-ssr@13861

@vue/compiler-vapor

npm i https://pkg.pr.new/@vue/compiler-vapor@13861

@vue/reactivity

npm i https://pkg.pr.new/@vue/reactivity@13861

@vue/runtime-core

npm i https://pkg.pr.new/@vue/runtime-core@13861

@vue/runtime-dom

npm i https://pkg.pr.new/@vue/runtime-dom@13861

@vue/runtime-vapor

npm i https://pkg.pr.new/@vue/runtime-vapor@13861

@vue/server-renderer

npm i https://pkg.pr.new/@vue/server-renderer@13861

@vue/shared

npm i https://pkg.pr.new/@vue/shared@13861

vue

npm i https://pkg.pr.new/vue@13861

@vue/compat

npm i https://pkg.pr.new/@vue/compat@13861

commit: 2ceaf78

@ferferga
Copy link
Contributor

Can you do the same for effectScope? Or perhaps the implementation of effectScope is enough and watch doesn't need this.

@OrbisK
Copy link
Contributor

OrbisK commented Sep 17, 2025

We should probably benchmark this.

It would be good to have this for effectScope and watch. This would allow us to get the signal with getCurrentScope.

const {signal} = getCurrentScope()

fetch(url, {signal})

Copy link
Member

@antfu antfu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally would love to see it landed!

@9romise 9romise changed the title feat(reactivity): add support for AbortController to watch feat(watch): add support for AbortController Sep 19, 2025
@9romise
Copy link
Author

9romise commented Sep 19, 2025

We should probably benchmark this.

It would be good to have this for effectScope and watch. This would allow us to get the signal with getCurrentScope.

const {signal} = getCurrentScope()

fetch(url, {signal})

That looks awesome! I’ve opened a new PR for effectScope — I’m not entirely sure if this aligns with what you had in mind, so feel free to take a look and leave your feedback there!

Thank you all for the reviews and suggestions! 💚

@9romise 9romise changed the base branch from main to minor September 23, 2025 05:35
Copy link

netlify bot commented Sep 23, 2025

Deploy Preview for vue-sfc-playground failed. Why did it fail? →

Name Link
🔨 Latest commit 04ce238
🔍 Latest deploy log https://app.netlify.com/projects/vue-sfc-playground/deploys/68d2375cdbe1320008f84f7d

@edison1105
Copy link
Member

LGTM.
Note: Documentation at https://vuejs.org/api/reactivity-core.html#watch needs to be updated when this feature is landed.

@edison1105 edison1105 moved this to In Progress in Next Minor Sep 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

5 participants