diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts
index a53c7a3e258..31c910d17d4 100644
--- a/packages/runtime-core/__tests__/hmr.spec.ts
+++ b/packages/runtime-core/__tests__/hmr.spec.ts
@@ -148,6 +148,49 @@ describe('hot module replacement', () => {
expect(mountSpy).toHaveBeenCalledTimes(1)
})
+ test('reload class component', async () => {
+ const root = nodeOps.createElement('div')
+ const childId = 'test4-child'
+ const unmountSpy = jest.fn()
+ const mountSpy = jest.fn()
+
+ class Child {
+ static __vccOpts: ComponentOptions = {
+ __hmrId: childId,
+ data() {
+ return { count: 0 }
+ },
+ unmounted: unmountSpy,
+ render: compileToFunction(`
{{ count }}
`)
+ }
+ }
+ createRecord(childId)
+
+ const Parent: ComponentOptions = {
+ render: () => h(Child)
+ }
+
+ render(h(Parent), root)
+ expect(serializeInner(root)).toBe(`0
`)
+
+ class UpdatedChild {
+ static __vccOpts: ComponentOptions = {
+ __hmrId: childId,
+ data() {
+ return { count: 1 }
+ },
+ mounted: mountSpy,
+ render: compileToFunction(`{{ count }}
`)
+ }
+ }
+
+ reload(childId, UpdatedChild)
+ await nextTick()
+ expect(serializeInner(root)).toBe(`1
`)
+ expect(unmountSpy).toHaveBeenCalledTimes(1)
+ expect(mountSpy).toHaveBeenCalledTimes(1)
+ })
+
// #1156 - static nodes should retain DOM element reference across updates
// when HMR is active
test('static el reference', async () => {
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index ac482d6707a..78abad60a8a 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -800,3 +800,7 @@ export function formatComponentName(
return name ? classify(name) : isRoot ? `App` : `Anonymous`
}
+
+export function isClassComponent(value: unknown): value is ClassComponent {
+ return isFunction(value) && '__vccOpts' in value
+}
diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts
index b831498baa4..df557313565 100644
--- a/packages/runtime-core/src/hmr.ts
+++ b/packages/runtime-core/src/hmr.ts
@@ -3,7 +3,9 @@ import {
ConcreteComponent,
ComponentInternalInstance,
ComponentOptions,
- InternalRenderFunction
+ InternalRenderFunction,
+ ClassComponent,
+ isClassComponent
} from './component'
import { queueJob, queuePostFlushCb } from './scheduler'
import { extend } from '@vue/shared'
@@ -83,7 +85,7 @@ function rerender(id: string, newRender?: Function) {
})
}
-function reload(id: string, newComp: ComponentOptions) {
+function reload(id: string, newComp: ComponentOptions | ClassComponent) {
const record = map.get(id)
if (!record) return
// Array.from creates a snapshot which avoids the set being mutated during
@@ -92,6 +94,7 @@ function reload(id: string, newComp: ComponentOptions) {
const comp = instance.type
if (!hmrDirtyComponents.has(comp)) {
// 1. Update existing comp definition to match new one
+ newComp = isClassComponent(newComp) ? newComp.__vccOpts : newComp
extend(comp, newComp)
for (const key in comp) {
if (!(key in newComp)) {
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index 9a11090a37c..56ff85f5eea 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -17,7 +17,8 @@ import {
Data,
ConcreteComponent,
ClassComponent,
- Component
+ Component,
+ isClassComponent
} from './component'
import { RawSlots } from './componentSlots'
import { isProxy, Ref, toRaw, ReactiveFlags } from '@vue/reactivity'
@@ -340,7 +341,7 @@ function _createVNode(
}
// class component normalization.
- if (isFunction(type) && '__vccOpts' in type) {
+ if (isClassComponent(type)) {
type = type.__vccOpts
}