Repository: vuejs/composition-api
Pull request #571 (2020-10-18 23:04:33)

feat(reactivity): add Vue.delete workaround
Workaround for notifying updates when removing keys in reactive objects and triggering view updates. Chinese README is missing.

antfu: Looking good. Could you add some unit tests for it? Thanks.
kiroushi: @antfu there you go.

Patch:
diff --git a/README.md b/README.md
index 210675f..476f68e 100644
--- a/README.md
+++ b/README.md
@@ -67,7 +67,28 @@ export default defineComponent({
 
 ### JSX/TSX
 
-To make JSX/TSX work with `@vue/composition-api`, check out [babel-preset-vca-jsx](https://github.com/luwanquan/babel-preset-vca-jsx) by [@luwanquan](https://github.com/luwanquan).
+JSX is now officially supported on [vuejs/jsx](https://github.com/vuejs/jsx). You can enabled it by following [this document](https://github.com/vuejs/jsx/tree/dev/packages/babel-preset-jsx#usage). A community maintained version can be found at [babel-preset-vca-jsx](https://github.com/luwanquan/babel-preset-vca-jsx) by [@luwanquan](https://github.com/luwanquan).
+
+To support TSX, create a declaration file with the following content in your project.
+
+```ts
+// file: shim-tsx.d.ts
+import Vue, { VNode } from 'vue';
+import { ComponentRenderProxy } from '@vue/composition-api';
+
+declare global {
+  namespace JSX {
+    interface Element extends VNode {}
+    interface ElementClass extends ComponentRenderProxy {}
+    interface ElementAttributesProperty {
+      $props: any; // specify the property name to use
+    }
+    interface IntrinsicElements {
+      [elem: string]: any;
+    }
+  }
+}
+```
 
 ## SSR
 
@@ -179,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'
@@ -193,6 +218,9 @@ const a = reactive({
 
 // add new reactive key
 set(a, 'bar', 1)
+
+// remove a key and trigger reactivity
+del(a, 'bar')
 ```
 
 </details>
@@ -420,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) {
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 362fd1d..dbd5538 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -66,7 +66,28 @@ export default defineComponent({
 
 ### JSX/TSX
 
-要使得 `@vue/composition-api` 支持 JSX/TSX，请前往查看由 [@luwanquan](https://github.com/luwanquan) 开发的 Babel 插件[babel-preset-vca-jsx](https://github.com/luwanquan/babel-preset-vca-jsx)。
+JSX 现已在 [vuejs/jsx](https://github.com/vuejs/jsx) 中官方支持。你可以根据[这篇文档](https://github.com/vuejs/jsx/tree/dev/packages/babel-preset-jsx#usage)开启支持。你也可以使用由 [@luwanquan](https://github.com/luwanquan) 维护的社区版本 [babel-preset-vca-jsx](https://github.com/luwanquan/babel-preset-vca-jsx)。
+
+对于 TSX 支持，请在你的项目中创建如下声明文件：
+
+```ts
+// file: shim-tsx.d.ts
+import Vue, { VNode } from 'vue';
+import { ComponentRenderProxy } from '@vue/composition-api';
+
+declare global {
+  namespace JSX {
+    interface Element extends VNode {}
+    interface ElementClass extends ComponentRenderProxy {}
+    interface ElementAttributesProperty {
+      $props: any; // specify the property name to use
+    }
+    interface IntrinsicElements {
+      [elem: string]: any;
+    }
+  }
+}
+```
 
 ## SSR
 
diff --git a/src/apis/index.ts b/src/apis/index.ts
index 5d0bda2..bf2e31e 100644
--- a/src/apis/index.ts
+++ b/src/apis/index.ts
@@ -1,4 +1,5 @@
 export {
+  del,
   isReactive,
   isRef,
   isRaw,
diff --git a/src/component/componentProps.ts b/src/component/componentProps.ts
index b404058..c27ff15 100644
--- a/src/component/componentProps.ts
+++ b/src/component/componentProps.ts
@@ -63,3 +63,12 @@ export type ExtractPropTypes<O> = O extends object
   ? { [K in RequiredKeys<O>]: InferPropType<O[K]> } &
       { [K in OptionalKeys<O>]?: InferPropType<O[K]> }
   : { [K in string]: any }
+
+type DefaultKeys<T> = {
+  [K in keyof T]: T[K] extends { default: any } ? K : never
+}[keyof T]
+
+// extract props which defined with default from prop options
+export type ExtractDefaultPropTypes<O> = O extends object
+  ? { [K in DefaultKeys<O>]: InferPropType<O[K]> }
+  : {}
diff --git a/src/component/componentProxy.ts b/src/component/componentProxy.ts
index d9105f1..82fb044 100644
--- a/src/component/componentProxy.ts
+++ b/src/component/componentProxy.ts
@@ -1,4 +1,4 @@
-import { ExtractPropTypes } from './componentProps'
+import { ExtractDefaultPropTypes, ExtractPropTypes } from './componentProps'
 import { ShallowUnwrapRef } from '..'
 import { Data } from './common'
 
@@ -22,10 +22,16 @@ export type ComponentRenderProxy<
   D = {}, // return from data()
   C extends ComputedOptions = {},
   M extends MethodOptions = {},
-  PublicProps = P
+  PublicProps = P,
+  Defaults = {},
+  MakeDefaultsOptional extends boolean = false
 > = {
   $data: D
-  $props: Readonly<P & PublicProps>
+  $props: Readonly<
+    MakeDefaultsOptional extends true
+      ? Partial<Defaults> & Omit<P & PublicProps, keyof Defaults>
+      : P & PublicProps
+  >
   $attrs: Data
 } & Readonly<P> &
   ShallowUnwrapRef<B> &
@@ -39,7 +45,12 @@ type VueConstructorProxy<PropsOptions, RawBindings> = VueConstructor & {
   new (...args: any[]): ComponentRenderProxy<
     ExtractPropTypes<PropsOptions>,
     ShallowUnwrapRef<RawBindings>,
-    ExtractPropTypes<PropsOptions>
+    ExtractPropTypes<PropsOptions>,
+    {},
+    {},
+    ExtractPropTypes<PropsOptions>,
+    ExtractDefaultPropTypes<PropsOptions>,
+    true
   >
 }
 
diff --git a/src/reactivity/del.ts b/src/reactivity/del.ts
new file mode 100644
index 0000000..d7a9607
--- /dev/null
+++ b/src/reactivity/del.ts
@@ -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()
+}
diff --git a/src/reactivity/index.ts b/src/reactivity/index.ts
index 162f2d2..f674197 100644
--- a/src/reactivity/index.ts
+++ b/src/reactivity/index.ts
@@ -24,3 +24,4 @@ export {
   ShallowUnwrapRef,
 } from './ref'
 export { set } from './set'
+export { del } from './del'
diff --git a/src/reactivity/set.ts b/src/reactivity/set.ts
index 8a4d6c7..f2a733c 100644
--- a/src/reactivity/set.ts
+++ b/src/reactivity/set.ts
@@ -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
diff --git a/src/runtimeContext.ts b/src/runtimeContext.ts
index 11e78bc..0d3e837 100644
--- a/src/runtimeContext.ts
+++ b/src/runtimeContext.ts
@@ -5,7 +5,16 @@ import { assert, hasOwn, warn } from './utils'
 let vueDependency: VueConstructor | undefined = undefined
 
 try {
-  vueDependency = require('vue')
+  const requiredVue = require('vue')
+  if (requiredVue && isVue(requiredVue)) {
+    vueDependency = requiredVue
+  } else if (
+    requiredVue &&
+    'default' in requiredVue &&
+    isVue(requiredVue.default)
+  ) {
+    vueDependency = requiredVue.default
+  }
 } catch {
   // not available
 }
@@ -15,6 +24,10 @@ let currentInstance: ComponentInstance | null = null
 
 const PluginInstalledFlag = '__composition_api_installed__'
 
+function isVue(obj: any): obj is VueConstructor {
+  return obj && typeof obj === 'function' && obj.name === 'Vue'
+}
+
 export function isPluginInstalled() {
   return !!vueConstructor
 }
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 64b5873..0bffd6d 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -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'
 }
@@ -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)
 }


Test patch:
diff --git a/test/v3/reactivity/del.spec.ts b/test/v3/reactivity/del.spec.ts
new file mode 100644
index 0000000..e7cf07a
--- /dev/null
+++ b/test/v3/reactivity/del.spec.ts
@@ -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])
+  })
+})