diff --git a/packages/runtime-core/__tests__/apiAsyncComponent.spec.ts b/packages/runtime-core/__tests__/apiAsyncComponent.spec.ts index 4f12d1c20be..6f16aa8d303 100644 --- a/packages/runtime-core/__tests__/apiAsyncComponent.spec.ts +++ b/packages/runtime-core/__tests__/apiAsyncComponent.spec.ts @@ -662,7 +662,7 @@ describe('api: defineAsyncComponent', () => { }) ) - const fooRef = ref() + const fooRef = ref(null) const toggle = ref(true) const root = nodeOps.createElement('div') createApp({ @@ -697,4 +697,51 @@ describe('api: defineAsyncComponent', () => { expect(serializeInner(root)).toBe('resolved') expect(fooRef.value.id).toBe('foo') }) + + // #3188 + test('the forwarded template ref should always exist when doing multi patching', async () => { + let resolve: (comp: Component) => void + const Foo = defineAsyncComponent( + () => + new Promise(r => { + resolve = r as any + }) + ) + + const fooRef = ref(null) + const toggle = ref(true) + const updater = ref(0) + + const root = nodeOps.createElement('div') + createApp({ + render: () => + toggle.value ? [h(Foo, { ref: fooRef }), updater.value] : null + }).mount(root) + + expect(serializeInner(root)).toBe('0') + expect(fooRef.value).toBe(null) + + resolve!({ + data() { + return { + id: 'foo' + } + }, + render: () => 'resolved' + }) + + await timeout() + expect(serializeInner(root)).toBe('resolved0') + expect(fooRef.value.id).toBe('foo') + + updater.value++ + await nextTick() + expect(serializeInner(root)).toBe('resolved1') + expect(fooRef.value.id).toBe('foo') + + toggle.value = false + await nextTick() + expect(serializeInner(root)).toBe('') + expect(fooRef.value).toBe(null) + }) }) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index b7973284f70..2a0b6a312ce 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -313,9 +313,13 @@ export const setRef = ( } let value: ComponentPublicInstance | RendererNode | Record | null - if (!vnode || isAsyncWrapper(vnode)) { + if (!vnode) { + // means unmount value = null } else { + // when mounting async components, nothing needs to be done, + // because the template ref is forwarded to inner component + if (isAsyncWrapper(vnode)) return if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { value = vnode.component!.exposed || vnode.component!.proxy } else {