Skip to content

Commit b41da83

Browse files
authored
feat(reactivity): add Vue.delete workaround (#571)
1 parent 964f9f3 commit b41da83

File tree

7 files changed

+113
-23
lines changed

7 files changed

+113
-23
lines changed

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,14 @@ a.list[1].count === 1 // true
200200

201201
<details>
202202
<summary>
203-
⚠️ <code>set</code> workaround for adding new reactive properties
203+
⚠️ <code>set</code> and <code>del</code> workaround for adding and deleting reactive properties
204204
</summary>
205205

206-
> ⚠️ 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.
206+
> ⚠️ 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).
207+
>
208+
> 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.
209+
>
210+
> 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`.
207211
208212
```ts
209213
import { reactive, set } from '@vue/composition-api'
@@ -214,6 +218,9 @@ const a = reactive({
214218

215219
// add new reactive key
216220
set(a, 'bar', 1)
221+
222+
// remove a key and trigger reactivity
223+
del(a, 'bar')
217224
```
218225

219226
</details>
@@ -441,7 +448,7 @@ app2.component('Bar', Bar) // equivalent to Vue.use('Bar', Bar)
441448
⚠️ <code>toRefs(props.foo.bar)</code> will incorrectly warn when acessing nested levels of props.
442449
⚠️ <code>isReactive(props.foo.bar)</code> will return false.
443450
</summary>
444-
451+
445452
```ts
446453
defineComponent({
447454
setup(props) {

src/apis/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export {
2+
del,
23
isReactive,
34
isRef,
45
isRaw,

src/reactivity/del.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { getVueConstructor } from '../runtimeContext'
2+
import { hasOwn, isPrimitive, isUndef, isValidArrayIndex } from '../utils'
3+
4+
/**
5+
* Delete a property and trigger change if necessary.
6+
*/
7+
export function del(target: any, key: any) {
8+
const Vue = getVueConstructor()
9+
const { warn } = Vue.util
10+
11+
if (__DEV__ && (isUndef(target) || isPrimitive(target))) {
12+
warn(
13+
`Cannot delete reactive property on undefined, null, or primitive value: ${target}`
14+
)
15+
}
16+
if (Array.isArray(target) && isValidArrayIndex(key)) {
17+
target.splice(key, 1)
18+
return
19+
}
20+
const ob = target.__ob__
21+
if (target._isVue || (ob && ob.vmCount)) {
22+
__DEV__ &&
23+
warn(
24+
'Avoid deleting properties on a Vue instance or its root $data ' +
25+
'- just set it to null.'
26+
)
27+
return
28+
}
29+
if (!hasOwn(target, key)) {
30+
return
31+
}
32+
delete target[key]
33+
if (!ob) {
34+
return
35+
}
36+
ob.dep.notify()
37+
}

src/reactivity/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export {
2424
ShallowUnwrapRef,
2525
} from './ref'
2626
export { set } from './set'
27+
export { del } from './del'

src/reactivity/set.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,7 @@
11
import { getVueConstructor } from '../runtimeContext'
2-
import { isArray } from '../utils'
2+
import { isArray, isPrimitive, isUndef, isValidArrayIndex } from '../utils'
33
import { defineAccessControl } from './reactive'
44

5-
function isUndef(v: any): boolean {
6-
return v === undefined || v === null
7-
}
8-
9-
function isPrimitive(value: any): boolean {
10-
return (
11-
typeof value === 'string' ||
12-
typeof value === 'number' ||
13-
// $flow-disable-line
14-
typeof value === 'symbol' ||
15-
typeof value === 'boolean'
16-
)
17-
}
18-
19-
function isValidArrayIndex(val: any): boolean {
20-
const n = parseFloat(String(val))
21-
return n >= 0 && Math.floor(n) === n && isFinite(val)
22-
}
23-
245
/**
256
* Set a property on an object. Adds the new property, triggers change
267
* notification and intercept it's subsequent access if the property doesn't

src/utils/utils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,25 @@ export function assert(condition: any, msg: string) {
4848
if (!condition) throw new Error(`[vue-composition-api] ${msg}`)
4949
}
5050

51+
export function isPrimitive(value: any): boolean {
52+
return (
53+
typeof value === 'string' ||
54+
typeof value === 'number' ||
55+
// $flow-disable-line
56+
typeof value === 'symbol' ||
57+
typeof value === 'boolean'
58+
)
59+
}
60+
5161
export function isArray<T>(x: unknown): x is T[] {
5262
return Array.isArray(x)
5363
}
5464

65+
export function isValidArrayIndex(val: any): boolean {
66+
const n = parseFloat(String(val))
67+
return n >= 0 && Math.floor(n) === n && isFinite(val)
68+
}
69+
5570
export function isObject(val: unknown): val is Record<any, any> {
5671
return val !== null && typeof val === 'object'
5772
}
@@ -64,6 +79,10 @@ export function isFunction(x: unknown): x is Function {
6479
return typeof x === 'function'
6580
}
6681

82+
export function isUndef(v: any): boolean {
83+
return v === undefined || v === null
84+
}
85+
6786
export function warn(msg: string, vm?: Vue) {
6887
Vue.util.warn(msg, vm)
6988
}

test/v3/reactivity/del.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { del, reactive, ref, watch } from '../../../src'
2+
3+
// Vue.delete workaround for triggering view updates on object property/array index deletion
4+
describe('reactivity/del', () => {
5+
it('should not trigger reactivity on native object member deletion', () => {
6+
const obj = reactive<{ a?: object }>({
7+
a: {},
8+
})
9+
const spy = jest.fn()
10+
watch(obj, spy, { deep: true, flush: 'sync' })
11+
delete obj.a // Vue 2 limitation
12+
expect(spy).not.toHaveBeenCalled()
13+
expect(obj).toStrictEqual({})
14+
})
15+
16+
it('should trigger reactivity when using del on reactive object', () => {
17+
const obj = reactive<{ a?: object }>({
18+
a: {},
19+
})
20+
const spy = jest.fn()
21+
watch(obj, spy, { deep: true, flush: 'sync' })
22+
del(obj, 'a')
23+
expect(spy).toBeCalledTimes(1)
24+
expect(obj).toStrictEqual({})
25+
})
26+
27+
it('should not remove element on array index and should not trigger reactivity', () => {
28+
const arr = ref([1, 2, 3])
29+
const spy = jest.fn()
30+
watch(arr, spy, { flush: 'sync' })
31+
delete arr.value[1] // Vue 2 limitation; workaround with .splice()
32+
expect(spy).not.toHaveBeenCalled()
33+
expect(arr.value).toEqual([1, undefined, 3])
34+
})
35+
36+
it('should trigger reactivity when using del on array', () => {
37+
const arr = ref([1, 2, 3])
38+
const spy = jest.fn()
39+
watch(arr, spy, { flush: 'sync' })
40+
del(arr.value, 1)
41+
expect(spy).toBeCalledTimes(1)
42+
expect(arr.value).toEqual([1, 3])
43+
})
44+
})

0 commit comments

Comments
 (0)