Repository: vuejs/composition-api
Pull request #723 (2021-06-05 08:26:40)

fix(observe): solve the Ref not unwrapping on the ssr side issue with recursive way.
Solving #721 with #722：
```
  it('result of #722', () => {
    const state = { old: ref(false) }
    set(state, 'new', ref(true))
    expect(JSON.stringify(state)).toBe(
      '{"old":{"value":false},"new":true}'  // 'old' is different from 'new'
    )
  })
```
the form of value of `old` key is different from `new` key.

if we solve #721 with recursive way:
```
  it('solving with recursive way', () => {
    const state = { old: ref(false) }
    set(state, 'new', ref(true))
    expect(JSON.stringify(state)).toBe(
      '{"old":{"value":false},"new":{"value":true}}'
    )
  })
```
for above case, should `{"old":{"value":false},"new":{"value":true}}` is more appropriate? @pikax @antfu Look forward your comments？



Patch:
diff --git a/src/reactivity/reactive.ts b/src/reactivity/reactive.ts
index 5fbf8fe..30ab9a0 100644
--- a/src/reactivity/reactive.ts
+++ b/src/reactivity/reactive.ts
@@ -127,14 +127,32 @@ export function observe<T>(obj: T): T {
 
   // in SSR, there is no __ob__. Mock for reactivity check
   if (!hasOwn(observed, '__ob__')) {
-    def(observed, '__ob__', mockObserver(observed))
+    mockReactivityDeep(observed)
   }
 
   return observed
 }
 
-export function createObserver() {
-  return observe<any>({}).__ob__
+/**
+ * Mock __ob__ for object recursively
+ */
+function mockReactivityDeep(obj: any, seen = new WeakMap<any, boolean>()) {
+  if (seen.has(obj)) return
+
+  def(obj, '__ob__', mockObserver(obj))
+  seen.set(obj, true)
+
+  for (const key of Object.keys(obj)) {
+    const value = obj[key]
+    if (
+      !(isPlainObject(value) || isArray(value)) ||
+      isRaw(value) ||
+      !Object.isExtensible(value)
+    ) {
+      continue
+    }
+    mockReactivityDeep(value)
+  }
 }
 
 function mockObserver(value: any = {}): any {
@@ -149,6 +167,10 @@ function mockObserver(value: any = {}): any {
   }
 }
 
+export function createObserver() {
+  return observe<any>({}).__ob__
+}
+
 export function shallowReactive<T extends object = any>(obj: T): T
 export function shallowReactive(obj: any): any {
   if (!isObject(obj)) {
diff --git a/src/reactivity/set.ts b/src/reactivity/set.ts
index d1baecf..02568bc 100644
--- a/src/reactivity/set.ts
+++ b/src/reactivity/set.ts
@@ -41,8 +41,7 @@ export function set<T>(target: AnyObject, key: any, val: T): T {
     return val
   }
   if (!ob) {
-    // If we are using `set` we can assume that the unwrapping is intended
-    defineAccessControl(target, key, val)
+    target[key] = val
     return val
   }
   defineReactive(ob.value, key, val)


Test patch:
diff --git a/test/ssr/ssrReactive.spec.ts b/test/ssr/ssrReactive.spec.ts
index b4325fa..fd32982 100644
--- a/test/ssr/ssrReactive.spec.ts
+++ b/test/ssr/ssrReactive.spec.ts
@@ -102,4 +102,20 @@ describe('SSR Reactive', () => {
       new: true,
     })
   })
+
+  // #721
+  it('should behave correctly for the nested ref in the object', () => {
+    const state = { old: ref(false) }
+    set(state, 'new', ref(true))
+    expect(JSON.stringify(state)).toBe(
+      '{"old":{"value":false},"new":{"value":true}}'
+    )
+  })
+
+  // #721
+  it('should behave correctly for ref of object', () => {
+    const state = ref({ old: ref(false) })
+    set(state.value, 'new', ref(true))
+    expect(JSON.stringify(state.value)).toBe('{"old":false,"new":true}')
+  })
 })