Repository: vuejs/composition-api (ts)

Issue:
Infinite update loop in 0.6.2 - 1.0.0-beta.6
The bug triggering "You may have an infinite update loop in a component render function" happens in unexpected places on unexpected reasons.

It did not happen with @vue/composition-api up until 0.6.1 and started happening in 0.6.2 and still persists.

I have recreated a minimal app with this problem
https://github.com/drafu/vue-composition-infinite-loop

**This bug strangely disappears if I:**
Remove A.vue component from App.vue
**OR**
Remove pattern prop from inside of A.vue component
**OR**
Remove div v-for iteration in App.vue
**OR**
Remove B.vue component from App.vue
**OR**
Remove emit from onMounted from B.vue

Which is highly strange all things considered, like, how is A.vue component, which is completely static, triggers infinite loop.

This bug is happening in some other places in my projects, but I cannot trace any consistent pattern.

Pull request description:
test: add test cases for #448
close #448, resolve #449

This is not long a bug in the latest implementation. Adding tests by #449.

Co-Authored-By: Carlos Rodrigues <carlos@hypermob.co.uk>

Patch:
diff --git a/src/apis/index.ts b/src/apis/index.ts
index 6c47069..5d0bda2 100644
--- a/src/apis/index.ts
+++ b/src/apis/index.ts
@@ -1,4 +1,26 @@
-export * from './state'
+export {
+  isReactive,
+  isRef,
+  isRaw,
+  markRaw,
+  reactive,
+  ref,
+  customRef,
+  Ref,
+  set,
+  shallowReactive,
+  shallowRef,
+  toRaw,
+  toRef,
+  toRefs,
+  triggerRef,
+  unref,
+  UnwrapRef,
+  isReadonly,
+  shallowReadonly,
+  proxyRefs,
+  ShallowUnwrapRef,
+} from '../reactivity'
 export * from './lifecycle'
 export * from './watch'
 export * from './computed'
diff --git a/src/apis/state.ts b/src/apis/state.ts
deleted file mode 100644
index 4c1e70b..0000000
--- a/src/apis/state.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-export {
-  isReactive,
-  isRef,
-  markRaw,
-  reactive,
-  ref,
-  customRef,
-  Ref,
-  set,
-  shallowReactive,
-  shallowRef,
-  toRaw,
-  toRef,
-  toRefs,
-  triggerRef,
-  unref,
-  UnwrapRef,
-  isReadonly,
-  shallowReadonly,
-  proxyRefs,
-  ShallowUnwrapRef,
-} from '../reactivity'
diff --git a/src/reactivity/reactive.ts b/src/reactivity/reactive.ts
index 8196a58..585f304 100644
--- a/src/reactivity/reactive.ts
+++ b/src/reactivity/reactive.ts
@@ -1,6 +1,6 @@
 import { AnyObject } from '../types/basic'
 import { getRegisteredVueOrDefault } from '../runtimeContext'
-import { isPlainObject, def, warn } from '../utils'
+import { isPlainObject, def, warn, hasOwn } from '../utils'
 import { isComponentInstance, defineComponentInstance } from '../utils/helper'
 import { RefKey } from '../utils/symbols'
 import { isRef, UnwrapRef } from './ref'
@@ -111,6 +111,11 @@ function observe<T>(obj: T): T {
     observed = vm._data.$$state
   }
 
+  // in SSR, there is no __ob__. Mock for reactivity check
+  if (!hasOwn(observed, '__ob__')) {
+    def(observed, '__ob__', {})
+  }
+
   return observed
 }
 
@@ -154,7 +159,7 @@ export function shallowReactive(obj: any): any {
       configurable: true,
       get: function getterHandler() {
         const value = getter ? getter.call(obj) : val
-        ob.dep.depend()
+        ob.dep?.depend()
         return value
       },
       set: function setterHandler(newVal) {
@@ -164,7 +169,7 @@ export function shallowReactive(obj: any): any {
         } else {
           val = newVal
         }
-        ob.dep.notify()
+        ob.dep?.notify()
       },
     })
   }


Test patch:
diff --git a/test/helpers/wait-for-update.js b/test/helpers/wait-for-update.js
index f426afd..df8334d 100644
--- a/test/helpers/wait-for-update.js
+++ b/test/helpers/wait-for-update.js
@@ -13,7 +13,7 @@ const Vue = require('vue')
 // })
 // .then(done)
 
-window.waitForUpdate = (initialCb) => {
+const waitForUpdate = (initialCb) => {
   let end
   const queue = initialCb ? [initialCb] : []
 
@@ -69,7 +69,11 @@ window.waitForUpdate = (initialCb) => {
   return chainer
 }
 
-exports.waitForUpdate = window.waitForUpdate
+if (typeof window !== 'undefined') {
+  window.waitForUpdate = waitForUpdate
+}
+
+exports.waitForUpdate = waitForUpdate
 
 function timeout(n) {
   return (next) => setTimeout(next, n)
diff --git a/test/setup.spec.js b/test/setup.spec.js
index 492feab..1e2b6a1 100644
--- a/test/setup.spec.js
+++ b/test/setup.spec.js
@@ -10,6 +10,8 @@ const {
   markRaw,
   toRaw,
   nextTick,
+  defineComponent,
+  onMounted,
 } = require('../src')
 const { sleep } = require('./helpers/utils')
 
@@ -886,4 +888,65 @@ describe('setup', () => {
     await nextTick()
     expect(vm.$el.textContent).toBe('2')
   })
+
+  // #448
+  it('should not cause infinite loop', async () => {
+    const A = defineComponent({
+      template: `<div></div>`,
+      props: {
+        pattern: {
+          type: RegExp,
+          required: true,
+        },
+      },
+
+      setup(props) {
+        return {
+          props,
+        }
+      },
+    })
+    const B = defineComponent({
+      template: `<div></div>`,
+      setup(props, { emit }) {
+        onMounted(() => {
+          emit('ev', true)
+        })
+
+        return {}
+      },
+    })
+
+    const vm = new Vue(
+      defineComponent({
+        components: {
+          A,
+          B,
+        },
+
+        template: `  <div>
+      <A :pattern="/./"/>
+  
+      <div
+        v-for="(k, v) in o.v"
+        :key="v"
+      >{{v}}</div>
+  
+      <B @ev="(v) => { o = v; }"/>
+    </div>`,
+        setup(props, { emit }) {
+          const o = ref([false])
+
+          return {
+            o,
+            emit,
+          }
+        },
+      })
+    ).$mount()
+
+    await vm.$nextTick()
+
+    expect(warn).not.toBeCalled()
+  })
 })
diff --git a/test/ssr/serverPrefetch.spec.js b/test/ssr/serverPrefetch.spec.js
index 9673053..578c786 100644
--- a/test/ssr/serverPrefetch.spec.js
+++ b/test/ssr/serverPrefetch.spec.js
@@ -4,8 +4,8 @@ const {
   ref,
   onServerPrefetch,
   getCurrentInstance,
-  provide,
-  inject,
+  isReactive,
+  reactive,
 } = require('../../src')
 
 function fetch(result) {
diff --git a/test/ssr/ssrReactive.spec.ts b/test/ssr/ssrReactive.spec.ts
new file mode 100644
index 0000000..befa01c
--- /dev/null
+++ b/test/ssr/ssrReactive.spec.ts
@@ -0,0 +1,47 @@
+/**
+ * @jest-environment node
+ */
+
+import Vue from '../vue'
+import { isReactive, reactive, ref, isRaw, isRef, shallowRef } from '../../src'
+import { createRenderer } from 'vue-server-renderer'
+
+describe('SSR Reactive', () => {
+  beforeEach(() => {
+    process.env.VUE_ENV = 'server'
+  })
+
+  it('should in SSR context', async () => {
+    expect(typeof window).toBe('undefined')
+    expect((Vue.observable({}) as any).__ob__).toBeUndefined()
+  })
+
+  it('should render', async () => {
+    const app = new Vue({
+      setup() {
+        return {
+          count: ref(42),
+        }
+      },
+      render(this: any, h) {
+        return h('div', this.count)
+      },
+    })
+
+    const serverRenderer = createRenderer()
+    const html = await serverRenderer.renderToString(app)
+    expect(html).toBe('<div data-server-rendered="true">42</div>')
+  })
+
+  it('reactive + isReactive', async () => {
+    const state = reactive({})
+    expect(isReactive(state)).toBe(true)
+    expect(isRaw(state)).toBe(false)
+  })
+
+  it('shallowRef + isRef', async () => {
+    const state = shallowRef({})
+    expect(isRef(state)).toBe(true)
+    expect(isRaw(state)).toBe(false)
+  })
+})