Repository: vuejs/composition-api (ts)

Issue:
`shallowReactive([])` does not return a array
code like this will easily break.

```js
const foo = shallowReactive([1, 2, 3])
foo.join(' ') // => runtime error here
```

https://github.com/vuejs/composition-api/blob/2c342d5778850904e1b35e680178b8aeb80b30d7/src/reactivity/reactive.ts#L158-L161

https://github.com/vuejs/composition-api/blob/2c342d5778850904e1b35e680178b8aeb80b30d7/src/reactivity/reactive.ts#L194-L196

Pull request description:
fix(shallowReactive): should keep array as array
fix #716.

Patch:
diff --git a/src/reactivity/reactive.ts b/src/reactivity/reactive.ts
index 3777958..4cc7280 100644
--- a/src/reactivity/reactive.ts
+++ b/src/reactivity/reactive.ts
@@ -156,7 +156,7 @@ export function shallowReactive(obj: any): any {
     return obj as any
   }
 
-  const observed = observe({})
+  const observed = observe(isArray(obj) ? [] : {})
   setupAccessControl(observed)
 
   const ob = (observed as any).__ob__
diff --git a/src/reactivity/set.ts b/src/reactivity/set.ts
index f2a733c..669487c 100644
--- a/src/reactivity/set.ts
+++ b/src/reactivity/set.ts
@@ -15,10 +15,16 @@ export function set<T>(target: any, key: any, val: T): T {
       `Cannot set reactive property on undefined, null, or primitive value: ${target}`
     )
   }
-  if (isArray(target) && isValidArrayIndex(key)) {
-    target.length = Math.max(target.length, key)
-    target.splice(key, 1, val)
-    return val
+  if (isArray(target)) {
+    if (isValidArrayIndex(key)) {
+      target.length = Math.max(target.length, key)
+      target.splice(key, 1, val)
+      return val
+    } else if (key === 'length' && (val as any) !== target.length) {
+      target.length = val as any
+      ;(target as any).__ob__?.dep.notify()
+      return val
+    }
   }
   if (key in target && !(key in Object.prototype)) {
     target[key] = val
@@ -34,7 +40,8 @@ export function set<T>(target: any, key: any, val: T): T {
     return val
   }
   if (!ob) {
-    target[key] = val
+    // If we are using `set` we can assume that the unwrapping is intended
+    defineAccessControl(target, key, val)
     return val
   }
   defineReactive(ob.value, key, val)
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 9f2e79e..804d442 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -71,9 +71,15 @@ export const isMap = (val: unknown): val is Map<any, any> =>
 export const isSet = (val: unknown): val is Set<any> =>
   toTypeString(val) === '[object Set]'
 
+const MAX_VALID_ARRAY_LENGTH = 4294967295 // Math.pow(2, 32) - 1
 export function isValidArrayIndex(val: any): boolean {
   const n = parseFloat(String(val))
-  return n >= 0 && Math.floor(n) === n && isFinite(val)
+  return (
+    n >= 0 &&
+    Math.floor(n) === n &&
+    isFinite(val) &&
+    n <= MAX_VALID_ARRAY_LENGTH
+  )
 }
 
 export function isObject(val: unknown): val is Record<any, any> {


Test patch:
diff --git a/test/ssr/ssrReactive.spec.ts b/test/ssr/ssrReactive.spec.ts
index 5c8510c..b4325fa 100644
--- a/test/ssr/ssrReactive.spec.ts
+++ b/test/ssr/ssrReactive.spec.ts
@@ -90,4 +90,16 @@ describe('SSR Reactive', () => {
 
     done()
   })
+
+  // #721
+  it('should behave correctly', () => {
+    const state = ref({ old: ref(false) })
+    set(state.value, 'new', ref(true))
+    // console.log(process.server, 'state.value', JSON.stringify(state.value))
+
+    expect(state.value).toMatchObject({
+      old: false,
+      new: true,
+    })
+  })
 })
diff --git a/test/v3/reactivity/del.spec.ts b/test/v3/reactivity/del.spec.ts
index e7cf07a..f590034 100644
--- a/test/v3/reactivity/del.spec.ts
+++ b/test/v3/reactivity/del.spec.ts
@@ -1,4 +1,4 @@
-import { del, reactive, ref, watch } from '../../../src'
+import { del, reactive, ref, watch, set, watchEffect } from '../../../src'
 
 // Vue.delete workaround for triggering view updates on object property/array index deletion
 describe('reactivity/del', () => {
@@ -41,4 +41,21 @@ describe('reactivity/del', () => {
     expect(spy).toBeCalledTimes(1)
     expect(arr.value).toEqual([1, 3])
   })
+
+  it('should trigger reactivity when using del on array to delete index out of valid array length', () => {
+    const arr = ref<number[]>([])
+    const MAX_VALID_ARRAY_LENGTH = Math.pow(2, 32) - 1
+    const NON_VALIDD_INDEX = MAX_VALID_ARRAY_LENGTH + 1
+    set(arr.value, NON_VALIDD_INDEX, 0)
+    const spy = jest.fn()
+    watchEffect(
+      () => {
+        spy(arr.value)
+      },
+      { flush: 'sync' }
+    )
+    expect(spy).toBeCalledTimes(1)
+    del(arr.value, NON_VALIDD_INDEX)
+    expect(spy).toBeCalledTimes(2)
+  })
 })
diff --git a/test/v3/reactivity/reactive.spec.ts b/test/v3/reactivity/reactive.spec.ts
index 75f9e82..4ae2e7d 100644
--- a/test/v3/reactivity/reactive.spec.ts
+++ b/test/v3/reactivity/reactive.spec.ts
@@ -206,6 +206,24 @@ describe('reactivity/reactive', () => {
       props.n = reactive({ foo: 2 })
       expect(isReactive(props.n)).toBe(true)
     })
+
+    test('should keep array as array', () => {
+      const arr = [1, 2, 3]
+      const shallowReactiveArr = shallowReactive(arr)
+      expect(Array.isArray(shallowReactiveArr)).toBe(true)
+      expect(shallowReactiveArr.join(' ')).toBe(arr.join(' '))
+    })
+
+    test('should trigger computed when changed', () => {
+      const arr = Array(10).fill(0)
+      const shallowReactiveArr = shallowReactive(arr)
+      const sum = computed(() =>
+        shallowReactiveArr.reduce((acc, cur) => acc + cur, 0)
+      )
+      expect(sum.value).toBe(0)
+      shallowReactiveArr[0] = 1
+      expect(sum.value).toBe(1)
+    })
   })
 
   test('should shallowReactive non-observable values', () => {
diff --git a/test/v3/reactivity/set.spec.ts b/test/v3/reactivity/set.spec.ts
new file mode 100644
index 0000000..24fb1b4
--- /dev/null
+++ b/test/v3/reactivity/set.spec.ts
@@ -0,0 +1,19 @@
+import { set, ref, watchEffect } from '../../../src'
+
+describe('reactivity/set', () => {
+  it('should trigger watchEffect when using set to change value of array length', () => {
+    const arr = ref([1, 2, 3])
+    const spy = jest.fn()
+    watchEffect(
+      () => {
+        spy(arr.value)
+      },
+      { flush: 'sync' }
+    )
+
+    expect(spy).toHaveBeenCalledTimes(1)
+    set(arr.value, 'length', 1)
+    expect(arr.value.length).toBe(1)
+    expect(spy).toHaveBeenCalledTimes(2)
+  })
+})