Skip to content

Commit 422f05e

Browse files
authored
fix(hmr): make hmr working with class components (#2144)
1 parent 57bdaa2 commit 422f05e

File tree

4 files changed

+55
-4
lines changed

4 files changed

+55
-4
lines changed

Diff for: packages/runtime-core/__tests__/hmr.spec.ts

+43
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,49 @@ describe('hot module replacement', () => {
148148
expect(mountSpy).toHaveBeenCalledTimes(1)
149149
})
150150

151+
test('reload class component', async () => {
152+
const root = nodeOps.createElement('div')
153+
const childId = 'test4-child'
154+
const unmountSpy = jest.fn()
155+
const mountSpy = jest.fn()
156+
157+
class Child {
158+
static __vccOpts: ComponentOptions = {
159+
__hmrId: childId,
160+
data() {
161+
return { count: 0 }
162+
},
163+
unmounted: unmountSpy,
164+
render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
165+
}
166+
}
167+
createRecord(childId)
168+
169+
const Parent: ComponentOptions = {
170+
render: () => h(Child)
171+
}
172+
173+
render(h(Parent), root)
174+
expect(serializeInner(root)).toBe(`<div>0</div>`)
175+
176+
class UpdatedChild {
177+
static __vccOpts: ComponentOptions = {
178+
__hmrId: childId,
179+
data() {
180+
return { count: 1 }
181+
},
182+
mounted: mountSpy,
183+
render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
184+
}
185+
}
186+
187+
reload(childId, UpdatedChild)
188+
await nextTick()
189+
expect(serializeInner(root)).toBe(`<div>1</div>`)
190+
expect(unmountSpy).toHaveBeenCalledTimes(1)
191+
expect(mountSpy).toHaveBeenCalledTimes(1)
192+
})
193+
151194
// #1156 - static nodes should retain DOM element reference across updates
152195
// when HMR is active
153196
test('static el reference', async () => {

Diff for: packages/runtime-core/src/component.ts

+4
Original file line numberDiff line numberDiff line change
@@ -800,3 +800,7 @@ export function formatComponentName(
800800

801801
return name ? classify(name) : isRoot ? `App` : `Anonymous`
802802
}
803+
804+
export function isClassComponent(value: unknown): value is ClassComponent {
805+
return isFunction(value) && '__vccOpts' in value
806+
}

Diff for: packages/runtime-core/src/hmr.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {
33
ConcreteComponent,
44
ComponentInternalInstance,
55
ComponentOptions,
6-
InternalRenderFunction
6+
InternalRenderFunction,
7+
ClassComponent,
8+
isClassComponent
79
} from './component'
810
import { queueJob, queuePostFlushCb } from './scheduler'
911
import { extend } from '@vue/shared'
@@ -83,7 +85,7 @@ function rerender(id: string, newRender?: Function) {
8385
})
8486
}
8587

86-
function reload(id: string, newComp: ComponentOptions) {
88+
function reload(id: string, newComp: ComponentOptions | ClassComponent) {
8789
const record = map.get(id)
8890
if (!record) return
8991
// Array.from creates a snapshot which avoids the set being mutated during
@@ -92,6 +94,7 @@ function reload(id: string, newComp: ComponentOptions) {
9294
const comp = instance.type
9395
if (!hmrDirtyComponents.has(comp)) {
9496
// 1. Update existing comp definition to match new one
97+
newComp = isClassComponent(newComp) ? newComp.__vccOpts : newComp
9598
extend(comp, newComp)
9699
for (const key in comp) {
97100
if (!(key in newComp)) {

Diff for: packages/runtime-core/src/vnode.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
Data,
1818
ConcreteComponent,
1919
ClassComponent,
20-
Component
20+
Component,
21+
isClassComponent
2122
} from './component'
2223
import { RawSlots } from './componentSlots'
2324
import { isProxy, Ref, toRaw, ReactiveFlags } from '@vue/reactivity'
@@ -340,7 +341,7 @@ function _createVNode(
340341
}
341342

342343
// class component normalization.
343-
if (isFunction(type) && '__vccOpts' in type) {
344+
if (isClassComponent(type)) {
344345
type = type.__vccOpts
345346
}
346347

0 commit comments

Comments
 (0)