Repository: vuejs/composition-api
Pull request #894 (2022-01-25 17:46:49)

fix(shallowReactive): don't trigger watchers for oldVal === newVal
Hi, I found another situation that's causing me infinite loops with this library but not with Vue 3


Patch:
diff --git a/src/reactivity/force.ts b/src/reactivity/force.ts
new file mode 100644
index 0000000..727e0f5
--- /dev/null
+++ b/src/reactivity/force.ts
@@ -0,0 +1,9 @@
+let _isForceTrigger = false
+
+export function isForceTrigger() {
+  return _isForceTrigger
+}
+
+export function setForceTrigger(v: boolean) {
+  _isForceTrigger = v
+}
diff --git a/src/reactivity/reactive.ts b/src/reactivity/reactive.ts
index 386cb29..b8dff25 100644
--- a/src/reactivity/reactive.ts
+++ b/src/reactivity/reactive.ts
@@ -14,6 +14,7 @@ import { isComponentInstance, defineComponentInstance } from '../utils/helper'
 import { RefKey } from '../utils/symbols'
 import { isRef, UnwrapRef } from './ref'
 import { rawSet, accessModifiedSet } from '../utils/sets'
+import { isForceTrigger } from './force'
 
 export function isRaw(obj: any): boolean {
   return Boolean(
@@ -189,7 +190,6 @@ export function shallowReactive(obj: any) {
   }
 
   const observed = observe(isArray(obj) ? [] : {})
-  setupAccessControl(observed)
 
   const ob = (observed as any).__ob__
 
@@ -214,6 +214,8 @@ export function shallowReactive(obj: any) {
       },
       set: function setterHandler(newVal: any) {
         if (getter && !setter) return
+        const value = getter ? getter.call(obj) : val
+        if (!isForceTrigger() && value === newVal) return
         if (setter) {
           setter.call(obj, newVal)
         } else {
diff --git a/src/reactivity/ref.ts b/src/reactivity/ref.ts
index 044a63d..4da6a55 100644
--- a/src/reactivity/ref.ts
+++ b/src/reactivity/ref.ts
@@ -3,6 +3,7 @@ import { proxy, isPlainObject, warn, def } from '../utils'
 import { reactive, isReactive, shallowReactive } from './reactive'
 import { readonlySet } from '../utils/sets'
 import { set } from './set'
+import { setForceTrigger } from './force'
 
 declare const _refBrand: unique symbol
 export interface Ref<T = any> {
@@ -184,7 +185,9 @@ export function shallowRef(raw?: unknown) {
 export function triggerRef(value: any) {
   if (!isRef(value)) return
 
+  setForceTrigger(true)
   value.value = value.value
+  setForceTrigger(false)
 }
 
 export function proxyRefs<T extends object>(


Test patch:
diff --git a/test/v3/reactivity/ref.spec.ts b/test/v3/reactivity/ref.spec.ts
index b4f9c95..1443f92 100644
--- a/test/v3/reactivity/ref.spec.ts
+++ b/test/v3/reactivity/ref.spec.ts
@@ -250,11 +250,29 @@ describe('reactivity/ref', () => {
     expect(dummy).toBe(1) // should not trigger yet
 
     // force trigger
-    // sref.value = sref.value;
     triggerRef(sref)
     expect(dummy).toBe(2)
   })
 
+  test('shallowRef noop when assignment is the same', () => {
+    const sref = shallowRef(1)
+    let calls = 0
+    watchEffect(
+      () => {
+        sref.value
+        calls++
+      },
+      { flush: 'sync' }
+    )
+    expect(calls).toBe(1)
+
+    sref.value = 2
+    expect(calls).toBe(2)
+
+    sref.value = 2
+    expect(calls).toBe(2)
+  })
+
   test('isRef', () => {
     expect(isRef(ref(1))).toBe(true)
     expect(isRef(computed(() => 1))).toBe(true)