Skip to content

fix(reactivity): handle Set with initial reactive values edge case#12393

Merged
edison1105 merged 4 commits intovuejs:mainfrom
shengxj1:reactivity-fix-set
Mar 9, 2026
Merged

fix(reactivity): handle Set with initial reactive values edge case#12393
edison1105 merged 4 commits intovuejs:mainfrom
shengxj1:reactivity-fix-set

Conversation

@shengxj1
Copy link
Contributor

@shengxj1 shengxj1 commented Nov 14, 2024

fix #8647

Summary by CodeRabbit

  • Bug Fixes

    • Improved Set handling to correctly process reactive objects during add operations. The implementation now properly compares both raw and reactive value forms to ensure accurate deduplication without unintended mutations.
  • Tests

    • Added test coverage for Sets with reactive initial values.

@edison1105 edison1105 added scope: reactivity 🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. wait changes labels Nov 15, 2024
@github-actions
Copy link

github-actions bot commented Nov 18, 2024

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 104 kB (-156 B) 39.4 kB (-45 B) 35.4 kB (-24 B)
vue.global.prod.js 162 kB (-156 B) 59.4 kB (-45 B) 52.8 kB (+9 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 47.9 kB (-140 B) 18.6 kB (-48 B) 17.1 kB (-37 B)
createApp 56 kB (-140 B) 21.7 kB (-47 B) 19.8 kB (-35 B)
createSSRApp 60.3 kB (-140 B) 23.4 kB (-44 B) 21.4 kB (-38 B)
defineCustomElement 61.6 kB (-156 B) 23.4 kB (-53 B) 21.4 kB (-32 B)
overall 70.4 kB (-140 B) 27 kB (-49 B) 24.6 kB (-71 B)

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 18, 2024

Open in StackBlitz

@vue/compiler-core

pnpm add https://pkg.pr.new/@vue/compiler-core@12393
npm i https://pkg.pr.new/@vue/compiler-core@12393
yarn add https://pkg.pr.new/@vue/compiler-core@12393.tgz

@vue/compiler-dom

pnpm add https://pkg.pr.new/@vue/compiler-dom@12393
npm i https://pkg.pr.new/@vue/compiler-dom@12393
yarn add https://pkg.pr.new/@vue/compiler-dom@12393.tgz

@vue/compiler-sfc

pnpm add https://pkg.pr.new/@vue/compiler-sfc@12393
npm i https://pkg.pr.new/@vue/compiler-sfc@12393
yarn add https://pkg.pr.new/@vue/compiler-sfc@12393.tgz

@vue/compiler-ssr

pnpm add https://pkg.pr.new/@vue/compiler-ssr@12393
npm i https://pkg.pr.new/@vue/compiler-ssr@12393
yarn add https://pkg.pr.new/@vue/compiler-ssr@12393.tgz

@vue/reactivity

pnpm add https://pkg.pr.new/@vue/reactivity@12393
npm i https://pkg.pr.new/@vue/reactivity@12393
yarn add https://pkg.pr.new/@vue/reactivity@12393.tgz

@vue/runtime-core

pnpm add https://pkg.pr.new/@vue/runtime-core@12393
npm i https://pkg.pr.new/@vue/runtime-core@12393
yarn add https://pkg.pr.new/@vue/runtime-core@12393.tgz

@vue/runtime-dom

pnpm add https://pkg.pr.new/@vue/runtime-dom@12393
npm i https://pkg.pr.new/@vue/runtime-dom@12393
yarn add https://pkg.pr.new/@vue/runtime-dom@12393.tgz

@vue/server-renderer

pnpm add https://pkg.pr.new/@vue/server-renderer@12393
npm i https://pkg.pr.new/@vue/server-renderer@12393
yarn add https://pkg.pr.new/@vue/server-renderer@12393.tgz

@vue/shared

pnpm add https://pkg.pr.new/@vue/shared@12393
npm i https://pkg.pr.new/@vue/shared@12393
yarn add https://pkg.pr.new/@vue/shared@12393.tgz

vue

pnpm add https://pkg.pr.new/vue@12393
npm i https://pkg.pr.new/vue@12393
yarn add https://pkg.pr.new/vue@12393.tgz

@vue/compat

pnpm add https://pkg.pr.new/@vue/compat@12393
npm i https://pkg.pr.new/@vue/compat@12393
yarn add https://pkg.pr.new/@vue/compat@12393.tgz

commit: 8e24f34

@edison1105 edison1105 changed the title fix(reactivity): check rawValue exists in target via set.add vuejs#8647 fix(reactivity): handle Set with initial reactive values edge case Nov 18, 2024
@edison1105 edison1105 added ready to merge The PR is ready to be merged. and removed wait changes labels Nov 18, 2024
@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

A bug fix that corrects Set behavior when initialized with reactive objects. Previously, adding a reactive object that already existed in the Set would incorrectly modify the Set despite .has() returning true. The fix modifies the add method to properly compare raw values, ensuring Set uniqueness is preserved. A test is added to verify the corrected behavior.

Changes

Cohort / File(s) Summary
Test Coverage
packages/reactivity/__tests__/reactive.spec.ts
Adds new test "observing Set with reactive initial value" verifying that adding an observed reactive object to a Set that already contains it does not increase the Set size, preserving Set uniqueness semantics.
Collection Handler Logic
packages/reactivity/src/collectionHandlers.ts
Modifies the add method in the non-readonly branch to compute valueToAdd using toRaw(value) for non-shallow reactive values. Updates key existence check to test both the raw-ified value and original value. Ensures raw values are inserted into the target and used in trigger operations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

p3-minor-bug

Poem

🐰 A Set once held items both raw and refined,
But proxies confused which duplicates to find.
Now unwrapped values keep uniqueness true,
No phantom additions—each item shines anew!
Hop-hop, the Set logic now works as it should! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the fix being implemented: handling the edge case of Set initialized with reactive values that violates Set uniqueness semantics.
Linked Issues check ✅ Passed The PR implementation addresses issue #8647 by checking raw value presence in target Set via valueToAdd logic, ensuring .add respects Set uniqueness when items are already present in reactive form.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing Set handling with reactive values: test additions in reactive.spec.ts and collectionHandlers.ts modifications for valueToAdd logic are directly related to issue #8647.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

♻️ Duplicate comments (1)
packages/reactivity/src/collectionHandlers.ts (1)

172-181: ⚠️ Potential issue | 🟠 Major

add() still diverges from has() for readonly/shallow proxies.

Line 176 only checks valueToAdd plus the original value. When value is readonly or shallow, valueToAdd === value, so the raw identity is never consulted. That means reactive(new Set([raw])).has(readonly(raw)) returns true, but add(readonly(raw)) still mutates the Set. The duplicate check needs to include toRaw(value) as well to keep add() aligned with has().

Possible fix
             const target = toRaw(this)
             const proto = getProto(target)
+            const rawValue = toRaw(value)
             const valueToAdd =
               !shallow && !isShallow(value) && !isReadonly(value)
-                ? toRaw(value)
+                ? rawValue
                 : value
             const hadKey =
               proto.has.call(target, valueToAdd) ||
-              (value !== valueToAdd && proto.has.call(target, value))
+              (hasChanged(value, valueToAdd) && proto.has.call(target, value)) ||
+              (hasChanged(rawValue, valueToAdd) &&
+                proto.has.call(target, rawValue))
             if (!hadKey) {
               target.add(valueToAdd)
               trigger(target, TriggerOpTypes.ADD, valueToAdd, valueToAdd)
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/reactivity/src/collectionHandlers.ts` around lines 172 - 181, The
add() branch uses valueToAdd and the original value to detect duplicates, but
when value is readonly/shallow (so valueToAdd === value) it never checks the raw
identity; update the duplicate detection in the hadKey calculation (the logic
around valueToAdd, proto.has, and value) to also test proto.has.call(target,
toRaw(value)) so that toRaw(value) is consulted like has() does; keep existing
checks for valueToAdd and value and then include toRaw(value) to ensure add()
aligns with has() for readonly/shallow proxies (referencing symbols: valueToAdd,
toRaw, proto.has, target.add, trigger, TriggerOpTypes.ADD).
🧹 Nitpick comments (1)
packages/reactivity/__tests__/reactive.spec.ts (1)

116-125: Assert that duplicate add() does not trigger effects.

This only proves cardinality stays at 1. It won't catch a regression where add() still emits TriggerOpTypes.ADD and re-runs dependents. A small effect(() => observedSet.size) counter would lock down the reactive contract this PR is fixing.

Proposed test strengthening
   test('observing Set with reactive initial value', () => {
     const observed = reactive({})
     const observedSet = reactive(new Set([observed]))
+    let runs = 0
+
+    effect(() => {
+      runs++
+      observedSet.size
+    })

     expect(observedSet.has(observed)).toBe(true)
     expect(observedSet.size).toBe(1)
+    expect(runs).toBe(1)

     // expect nothing happens
     observedSet.add(observed)
     expect(observedSet.size).toBe(1)
+    expect(runs).toBe(1)
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/reactivity/__tests__/reactive.spec.ts` around lines 116 - 125, The
test currently only checks Set cardinality but not whether duplicate add() emits
a reactive trigger; add an effect that depends on observedSet.size (e.g. let
runs = 0; effect(() => { void observedSet.size; runs++ })) before calling
observedSet.add(observed), then call observedSet.add(observed) and assert runs
did not increase (stays the same) to ensure no TriggerOpTypes.ADD side-effect;
locate this around the existing test using identifiers observed, observedSet,
reactive, and effect.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/reactivity/src/collectionHandlers.ts`:
- Around line 172-181: The add() branch uses valueToAdd and the original value
to detect duplicates, but when value is readonly/shallow (so valueToAdd ===
value) it never checks the raw identity; update the duplicate detection in the
hadKey calculation (the logic around valueToAdd, proto.has, and value) to also
test proto.has.call(target, toRaw(value)) so that toRaw(value) is consulted like
has() does; keep existing checks for valueToAdd and value and then include
toRaw(value) to ensure add() aligns with has() for readonly/shallow proxies
(referencing symbols: valueToAdd, toRaw, proto.has, target.add, trigger,
TriggerOpTypes.ADD).

---

Nitpick comments:
In `@packages/reactivity/__tests__/reactive.spec.ts`:
- Around line 116-125: The test currently only checks Set cardinality but not
whether duplicate add() emits a reactive trigger; add an effect that depends on
observedSet.size (e.g. let runs = 0; effect(() => { void observedSet.size;
runs++ })) before calling observedSet.add(observed), then call
observedSet.add(observed) and assert runs did not increase (stays the same) to
ensure no TriggerOpTypes.ADD side-effect; locate this around the existing test
using identifiers observed, observedSet, reactive, and effect.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 70c73362-eed7-404e-b512-6f16417d49d1

📥 Commits

Reviewing files that changed from the base of the PR and between cea3cf7 and 99a31d4.

📒 Files selected for processing (2)
  • packages/reactivity/__tests__/reactive.spec.ts
  • packages/reactivity/src/collectionHandlers.ts

@edison1105 edison1105 merged commit 5dc27ca into vuejs:main Mar 9, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. ready to merge The PR is ready to be merged. scope: reactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unintuitive Proxy behavior violating set item uniqueness constraint

2 participants