diff --git a/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts b/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts index 64cb9c63014..a7ae7a06bfd 100644 --- a/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts +++ b/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts @@ -1,4 +1,6 @@ import { + KeepAlive, + defineAsyncComponent, defineComponent, h, nextTick, @@ -538,4 +540,68 @@ describe('api: template refs', () => { '
[object Object],[object Object]
', ) }) + + test('with async component which nested in KeepAlive', async () => { + const AsyncComp = defineAsyncComponent( + () => + new Promise(resolve => + setTimeout(() => + resolve( + defineComponent({ + setup(_, { expose }) { + expose({ + name: 'AsyncComp', + }) + return () => h('div') + }, + }) as any, + ), + ), + ), + ) + + const Comp = defineComponent({ + setup(_, { expose }) { + expose({ + name: 'Comp', + }) + return () => h('div') + }, + }) + + const toggle = ref(false) + const instanceRef = ref(null) + + const App = { + render: () => { + return h(KeepAlive, () => + toggle.value + ? h(AsyncComp, { ref: instanceRef }) + : h(Comp, { ref: instanceRef }), + ) + }, + } + + const root = nodeOps.createElement('div') + render(h(App), root) + expect(instanceRef.value.name).toBe('Comp') + + // switch to async component + toggle.value = true + await nextTick() + expect(instanceRef.value).toBe(null) + + await new Promise(r => setTimeout(r)) + expect(instanceRef.value.name).toBe('AsyncComp') + + // switch back to normal component + toggle.value = false + await nextTick() + expect(instanceRef.value.name).toBe('Comp') + + // switch to async component again + toggle.value = true + await nextTick() + expect(instanceRef.value.name).toBe('AsyncComp') + }) }) diff --git a/packages/runtime-core/src/rendererTemplateRef.ts b/packages/runtime-core/src/rendererTemplateRef.ts index bffe1a25321..70f772d9daf 100644 --- a/packages/runtime-core/src/rendererTemplateRef.ts +++ b/packages/runtime-core/src/rendererTemplateRef.ts @@ -15,7 +15,7 @@ import { isRef, toRaw } from '@vue/reactivity' import { ErrorCodes, callWithErrorHandling } from './errorHandling' import type { SchedulerJob } from './scheduler' import { queuePostRenderEffect } from './renderer' -import { getComponentPublicInstance } from './component' +import { type ComponentOptions, getComponentPublicInstance } from './component' import { knownTemplateRefs } from './helpers/useTemplateRef' /** @@ -42,6 +42,17 @@ export function setRef( } if (isAsyncWrapper(vnode) && !isUnmount) { + // #4999 if an async component already resolved and cached by KeepAlive, + // we need to set the ref to inner component + if ( + vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE && + (vnode.type as ComponentOptions).__asyncResolved && + vnode.component!.subTree.component + ) { + setRef(rawRef, oldRawRef, parentSuspense, vnode.component!.subTree) + return + } + // when mounting async components, nothing needs to be done, // because the template ref is forwarded to inner component return