Skip to content

Commit

Permalink
feat(reactivity): add Vue.delete workaround (#571)
Browse files Browse the repository at this point in the history
  • Loading branch information
kiroushi authored Oct 21, 2020
1 parent 964f9f3 commit b41da83
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 23 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,14 @@ a.list[1].count === 1 // true

<details>
<summary>
⚠️ <code>set</code> workaround for adding new reactive properties
⚠️ <code>set</code> and <code>del</code> workaround for adding and deleting reactive properties
</summary>

> ⚠️ Warning: `set` does NOT exist in Vue 3. We provide it as a workaround here, due to the limitation of [Vue 2.x reactivity system](https://vuejs.org/v2/guide/reactivity.html#For-Objects). In Vue 2, you will need to call `set` to track new keys on an `object`(similar to `Vue.set` but for `reactive objects` created by the Composition API). In Vue 3, you can just assign them like normal objects.
> ⚠️ Warning: `set` and `del` do NOT exist in Vue 3. We provide them as a workaround here, due to the limitation of [Vue 2.x reactivity system](https://vuejs.org/v2/guide/reactivity.html#For-Objects).
>
> In Vue 2, you will need to call `set` to track new keys on an `object`(similar to `Vue.set` but for `reactive objects` created by the Composition API). In Vue 3, you can just assign them like normal objects.
>
> Similarly, in Vue 2 you will need to call `del` to [ensure a key deletion triggers view updates](https://vuejs.org/v2/api/#Vue-delete) in reactive objects (similar to `Vue.delete` but for `reactive objects` created by the Composition API). In Vue 3 you can just delete them by calling `delete foo.bar`.
```ts
import { reactive, set } from '@vue/composition-api'
Expand All @@ -214,6 +218,9 @@ const a = reactive({

// add new reactive key
set(a, 'bar', 1)

// remove a key and trigger reactivity
del(a, 'bar')
```

</details>
Expand Down Expand Up @@ -441,7 +448,7 @@ app2.component('Bar', Bar) // equivalent to Vue.use('Bar', Bar)
⚠️ <code>toRefs(props.foo.bar)</code> will incorrectly warn when acessing nested levels of props.
⚠️ <code>isReactive(props.foo.bar)</code> will return false.
</summary>

```ts
defineComponent({
setup(props) {
Expand Down
1 change: 1 addition & 0 deletions src/apis/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {
del,
isReactive,
isRef,
isRaw,
Expand Down
37 changes: 37 additions & 0 deletions src/reactivity/del.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { getVueConstructor } from '../runtimeContext'
import { hasOwn, isPrimitive, isUndef, isValidArrayIndex } from '../utils'

/**
* Delete a property and trigger change if necessary.
*/
export function del(target: any, key: any) {
const Vue = getVueConstructor()
const { warn } = Vue.util

if (__DEV__ && (isUndef(target) || isPrimitive(target))) {
warn(
`Cannot delete reactive property on undefined, null, or primitive value: ${target}`
)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = target.__ob__
if (target._isVue || (ob && ob.vmCount)) {
__DEV__ &&
warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
1 change: 1 addition & 0 deletions src/reactivity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export {
ShallowUnwrapRef,
} from './ref'
export { set } from './set'
export { del } from './del'
21 changes: 1 addition & 20 deletions src/reactivity/set.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,7 @@
import { getVueConstructor } from '../runtimeContext'
import { isArray } from '../utils'
import { isArray, isPrimitive, isUndef, isValidArrayIndex } from '../utils'
import { defineAccessControl } from './reactive'

function isUndef(v: any): boolean {
return v === undefined || v === null
}

function isPrimitive(value: any): boolean {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}

function isValidArrayIndex(val: any): boolean {
const n = parseFloat(String(val))
return n >= 0 && Math.floor(n) === n && isFinite(val)
}

/**
* Set a property on an object. Adds the new property, triggers change
* notification and intercept it's subsequent access if the property doesn't
Expand Down
19 changes: 19 additions & 0 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,25 @@ export function assert(condition: any, msg: string) {
if (!condition) throw new Error(`[vue-composition-api] ${msg}`)
}

export function isPrimitive(value: any): boolean {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}

export function isArray<T>(x: unknown): x is T[] {
return Array.isArray(x)
}

export function isValidArrayIndex(val: any): boolean {
const n = parseFloat(String(val))
return n >= 0 && Math.floor(n) === n && isFinite(val)
}

export function isObject(val: unknown): val is Record<any, any> {
return val !== null && typeof val === 'object'
}
Expand All @@ -64,6 +79,10 @@ export function isFunction(x: unknown): x is Function {
return typeof x === 'function'
}

export function isUndef(v: any): boolean {
return v === undefined || v === null
}

export function warn(msg: string, vm?: Vue) {
Vue.util.warn(msg, vm)
}
Expand Down
44 changes: 44 additions & 0 deletions test/v3/reactivity/del.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { del, reactive, ref, watch } from '../../../src'

// Vue.delete workaround for triggering view updates on object property/array index deletion
describe('reactivity/del', () => {
it('should not trigger reactivity on native object member deletion', () => {
const obj = reactive<{ a?: object }>({
a: {},
})
const spy = jest.fn()
watch(obj, spy, { deep: true, flush: 'sync' })
delete obj.a // Vue 2 limitation
expect(spy).not.toHaveBeenCalled()
expect(obj).toStrictEqual({})
})

it('should trigger reactivity when using del on reactive object', () => {
const obj = reactive<{ a?: object }>({
a: {},
})
const spy = jest.fn()
watch(obj, spy, { deep: true, flush: 'sync' })
del(obj, 'a')
expect(spy).toBeCalledTimes(1)
expect(obj).toStrictEqual({})
})

it('should not remove element on array index and should not trigger reactivity', () => {
const arr = ref([1, 2, 3])
const spy = jest.fn()
watch(arr, spy, { flush: 'sync' })
delete arr.value[1] // Vue 2 limitation; workaround with .splice()
expect(spy).not.toHaveBeenCalled()
expect(arr.value).toEqual([1, undefined, 3])
})

it('should trigger reactivity when using del on array', () => {
const arr = ref([1, 2, 3])
const spy = jest.fn()
watch(arr, spy, { flush: 'sync' })
del(arr.value, 1)
expect(spy).toBeCalledTimes(1)
expect(arr.value).toEqual([1, 3])
})
})

0 comments on commit b41da83

Please sign in to comment.