Repository: vuejs/composition-api
Pull request #843 (2021-11-02 17:30:48)

fix: changing prop causes rerender to lose attributes #840
fix https://github.com/vuejs/composition-api/issues/840
This problem is triggered when the user installs the composition API and uses the template method to define that the key of data contains attrs

Like test case description


Patch:
diff --git a/src/mixin.ts b/src/mixin.ts
index e560796..54f0c21 100644
--- a/src/mixin.ts
+++ b/src/mixin.ts
@@ -34,7 +34,7 @@ export function mixin(Vue: VueConstructor) {
       updateTemplateRef(this)
     },
     beforeUpdate() {
-      updateVmAttrs(this as ComponentInstance, this as SetupContext)
+      updateVmAttrs(this as ComponentInstance)
     },
     updated(this: ComponentInstance) {
       updateTemplateRef(this)
diff --git a/src/utils/instance.ts b/src/utils/instance.ts
index 47d135f..5544f97 100644
--- a/src/utils/instance.ts
+++ b/src/utils/instance.ts
@@ -103,18 +103,22 @@ export function updateTemplateRef(vm: ComponentInstance) {
   vmStateManager.set(vm, 'refs', validNewKeys)
 }
 
-export function updateVmAttrs(vm: ComponentInstance, ctx: SetupContext) {
-  if (!vm || !ctx) {
+export function updateVmAttrs(vm: ComponentInstance, ctx?: SetupContext) {
+  if (!vm) {
     return
   }
   let attrBindings = vmStateManager.get(vm, 'attrBindings')
+  if (!attrBindings && !ctx) {
+    // fix 840
+    return
+  }
   if (!attrBindings) {
     const observedData = reactive({})
-    vmStateManager.set(vm, 'attrBindings', observedData)
-    attrBindings = observedData
+    attrBindings = { ctx: ctx!, data: observedData }
+    vmStateManager.set(vm, 'attrBindings', attrBindings)
     proxy(ctx, 'attrs', {
       get: () => {
-        return attrBindings
+        return attrBindings?.data
       },
       set() {
         __DEV__ &&
@@ -128,8 +132,8 @@ export function updateVmAttrs(vm: ComponentInstance, ctx: SetupContext) {
 
   const source = vm.$attrs
   for (const attr of Object.keys(source)) {
-    if (!hasOwn(attrBindings!, attr)) {
-      proxy(attrBindings, attr, {
+    if (!hasOwn(attrBindings.data, attr)) {
+      proxy(attrBindings.data, attr, {
         get: () => {
           // to ensure it always return the latest value
           return vm.$attrs[attr]
diff --git a/src/utils/vmStateManager.ts b/src/utils/vmStateManager.ts
index 5a9d80f..3896335 100644
--- a/src/utils/vmStateManager.ts
+++ b/src/utils/vmStateManager.ts
@@ -1,9 +1,13 @@
 import { ComponentInstance, Data } from '../component'
+import { SetupContext } from '../runtimeContext'
 
 export interface VfaState {
   refs?: string[]
   rawBindings?: Data
-  attrBindings?: Data
+  attrBindings?: {
+    ctx: SetupContext
+    data: Data
+  }
   slots?: string[]
 }
 


Test patch:
diff --git a/test/setup.spec.js b/test/setup.spec.js
index 5ae0e6b..4f55b03 100644
--- a/test/setup.spec.js
+++ b/test/setup.spec.js
@@ -1304,4 +1304,81 @@ describe('setup', () => {
       lastName: 'xiao',
     })
   })
+
+  // #840
+  it('changing prop causes rerender to lose attributes', async () => {
+    let childAttrs = []
+    const Parent = {
+      computed: {
+        attrs() {
+          return {
+            'data-type': this.type,
+          }
+        },
+      },
+      props: {
+        type: {
+          type: String,
+          required: true,
+        },
+      },
+      mounted() {
+        childAttrs.push(this.attrs)
+      },
+      updated() {
+        childAttrs.push(this.attrs)
+      },
+      template: '<div v-bind="attrs">Parent<slot /></div>',
+    }
+    const Child = {
+      props: {
+        type: {
+          type: String,
+          required: true,
+        },
+      },
+      computed: {
+        attrs() {
+          return {
+            'data-type': this.type,
+          }
+        },
+      },
+      data() {
+        return {
+          update: 0,
+        }
+      },
+      template: '  <div v-bind="attrs">Child</div>',
+    }
+
+    const App = {
+      name: 'App',
+      data() {
+        return { parentType: 'parent' }
+      },
+      async mounted() {
+        await sleep(300)
+        this.parentType = 'parent-click'
+      },
+
+      components: {
+        Parent,
+        Child,
+      },
+      template: ` <div id="app">
+        <Parent :type="parentType">
+          <Child type="child"/>
+        </Parent>
+      </div>`,
+    }
+    const vm = new Vue(App).$mount()
+
+    await sleep(1000)
+    await vm.$nextTick()
+    expect(childAttrs).toStrictEqual([
+      { 'data-type': 'parent' },
+      { 'data-type': 'parent-click' },
+    ])
+  })
 })