diff --git a/README.md b/README.md
index 719efad6..2ebd041e 100644
--- a/README.md
+++ b/README.md
@@ -152,6 +152,33 @@ b.list[0].count.value === 0 // true
+
+
+✅ Should always use ref
in a reactive
when working with Array
+
+
+```js
+const a = reactive({
+ list: [
+ reactive({
+ count: ref(0),
+ }),
+ ]
+})
+// unwrapped
+a.list[0].count === 0 // true
+
+a.list.push(
+ reactive({
+ count: ref(1),
+ })
+)
+// unwrapped
+a.list[1].count === 1 // true
+```
+
+
+
⚠️ `set` workaround for adding new reactive properties
diff --git a/src/mixin.ts b/src/mixin.ts
index a199ecf1..17fe2e80 100644
--- a/src/mixin.ts
+++ b/src/mixin.ts
@@ -5,7 +5,7 @@ import {
SetupFunction,
Data,
} from './component'
-import { isRef, isReactive, toRefs } from './reactivity'
+import { isRef, isReactive, toRefs, isRaw } from './reactivity'
import {
isPlainObject,
assert,
@@ -14,6 +14,7 @@ import {
isFunction,
isObject,
def,
+ isArray,
} from './utils'
import { ref } from './apis'
import vmStateManager from './utils/vmStateManager'
@@ -23,6 +24,7 @@ import {
resolveScopedSlots,
asVmProperty,
} from './utils/instance'
+import { getVueConstructor } from './runtimeContext'
import { createObserver } from './reactivity/reactive'
export function mixin(Vue: VueConstructor) {
@@ -121,7 +123,13 @@ export function mixin(Vue: VueConstructor) {
bindingValue = bindingValue.bind(vm)
} else if (!isObject(bindingValue)) {
bindingValue = ref(bindingValue)
+ } else if (hasReactiveArrayChild(bindingValue)) {
+ // creates a custom reactive properties without make the object explicitly reactive
+ // NOTE we should try to avoid this, better implementation needed
+ customReactive(bindingValue)
}
+ } else if (isArray(bindingValue)) {
+ bindingValue = ref(bindingValue)
}
}
asVmProperty(vm, name, bindingValue)
@@ -140,6 +148,45 @@ export function mixin(Vue: VueConstructor) {
}
}
+ function customReactive(target: object) {
+ if (
+ !isPlainObject(target) ||
+ isRef(target) ||
+ isReactive(target) ||
+ isRaw(target)
+ )
+ return
+ const Vue = getVueConstructor()
+ const defineReactive = Vue.util.defineReactive
+
+ Object.keys(target).forEach((k) => {
+ const val = target[k]
+ defineReactive(target, k, val)
+ if (val) {
+ customReactive(val)
+ }
+ return
+ })
+ }
+
+ function hasReactiveArrayChild(target: object, visited = new Map()): boolean {
+ if (visited.has(target)) {
+ return visited.get(target)
+ }
+ visited.set(target, false)
+ if (Array.isArray(target) && isReactive(target)) {
+ visited.set(target, true)
+ return true
+ }
+
+ if (!isPlainObject(target) || isRaw(target)) {
+ return false
+ }
+ return Object.keys(target).some((x) =>
+ hasReactiveArrayChild(target[x], visited)
+ )
+ }
+
function createSetupContext(
vm: ComponentInstance & { [x: string]: any }
): SetupContext {
diff --git a/src/reactivity/reactive.ts b/src/reactivity/reactive.ts
index 857dc20e..10064556 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, hasOwn } from '../utils'
+import { isPlainObject, def, warn, isArray, hasOwn } from '../utils'
import { isComponentInstance, defineComponentInstance } from '../utils/helper'
import { RefKey } from '../utils/symbols'
import { isRef, UnwrapRef } from './ref'
@@ -130,7 +130,11 @@ export function shallowReactive(obj: any): any {
return
}
- if (!isPlainObject(obj) || isRaw(obj) || !Object.isExtensible(obj)) {
+ if (
+ !(isPlainObject(obj) || isArray(obj)) ||
+ isRaw(obj) ||
+ !Object.isExtensible(obj)
+ ) {
return obj as any
}
@@ -190,7 +194,11 @@ export function reactive(obj: T): UnwrapRef {
return
}
- if (!isPlainObject(obj) || isRaw(obj) || !Object.isExtensible(obj)) {
+ if (
+ !(isPlainObject(obj) || isArray(obj)) ||
+ isRaw(obj) ||
+ !Object.isExtensible(obj)
+ ) {
return obj as any
}
@@ -201,7 +209,7 @@ export function reactive(obj: T): UnwrapRef {
export function shallowReadonly(obj: T): Readonly
export function shallowReadonly(obj: any): any {
- if (!isPlainObject(obj) || !Object.isExtensible(obj)) {
+ if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) {
return obj
}
@@ -254,7 +262,7 @@ export function shallowReadonly(obj: any): any {
* Make sure obj can't be a reactive
*/
export function markRaw(obj: T): T {
- if (!isPlainObject(obj) || !Object.isExtensible(obj)) {
+ if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) {
return obj
}
diff --git a/test/setup.spec.js b/test/setup.spec.js
index b3bd34e8..9bd1c7f4 100644
--- a/test/setup.spec.js
+++ b/test/setup.spec.js
@@ -896,6 +896,110 @@ describe('setup', () => {
expect(vm.$el.textContent).toBe('2')
})
+ // #524
+ it('should work with reactive arrays.', async () => {
+ const opts = {
+ template: `{{items.length}}
`,
+ setup() {
+ const items = reactive([])
+
+ setTimeout(() => {
+ items.push(2)
+ }, 1)
+
+ return {
+ items,
+ }
+ },
+ }
+ const Constructor = Vue.extend(opts).extend({})
+
+ const vm = new Vue(Constructor).$mount()
+ expect(vm.$el.textContent).toBe('0')
+ await sleep(10)
+ await nextTick()
+ expect(vm.$el.textContent).toBe('1')
+ })
+
+ it('should work with reactive array nested', async () => {
+ const opts = {
+ template: `{{a.items.length}}
`,
+ setup() {
+ const items = reactive([])
+
+ setTimeout(() => {
+ items.push(2)
+ }, 1)
+
+ return {
+ a: {
+ items,
+ },
+ }
+ },
+ }
+ const Constructor = Vue.extend(opts).extend({})
+
+ const vm = new Vue(Constructor).$mount()
+ expect(vm.$el.textContent).toBe('0')
+ await sleep(10)
+ await nextTick()
+ expect(vm.$el.textContent).toBe('1')
+ })
+
+ it('should not unwrap reactive array nested', async () => {
+ const opts = {
+ template: `{{a.items}}
`,
+ setup() {
+ const items = reactive([])
+
+ setTimeout(() => {
+ items.push(ref(1))
+ }, 1)
+
+ return {
+ a: {
+ items,
+ },
+ }
+ },
+ }
+ const Constructor = Vue.extend(opts).extend({})
+
+ const vm = new Vue(Constructor).$mount()
+ expect(vm.$el.textContent).toBe('[]')
+ await sleep(10)
+ await nextTick()
+ expect(JSON.parse(vm.$el.textContent)).toStrictEqual([{ value: 1 }])
+ })
+
+ // TODO make this pass
+ // it('should work with computed', async ()=>{
+ // const opts = {
+ // template: `{{len}}
`,
+ // setup() {
+ // const array = reactive([]);
+ // const len = computed(()=> array.length);
+
+ // setTimeout(() => {
+ // array.push(2)
+ // }, 1)
+
+ // return {
+ // len
+ // }
+ // },
+ // }
+ // const Constructor = Vue.extend(opts).extend({})
+
+ // const vm = new Vue(Constructor).$mount()
+ // expect(vm.$el.textContent).toBe('0')
+ // await sleep(10)
+ // await nextTick()
+ // expect(vm.$el.textContent).toBe('1')
+ // })
+
+
// #448
it('should not cause infinite loop', async () => {
const A = defineComponent({