diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 65e801de277..563c91a179d 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -2230,5 +2230,57 @@ describe('Suspense', () => { fallback: [h('div'), h('div')], }) }) + + // #13559 + test('renders multiple async components in Suspense with v-for and updates on items change', async () => { + const CompAsyncSetup = defineAsyncComponent({ + props: ['item'], + render(ctx: any) { + return h('div', ctx.item.name) + }, + }) + + const items = ref([ + { id: 1, name: '111' }, + { id: 2, name: '222' }, + { id: 3, name: '333' }, + ]) + + const Comp = { + setup() { + return () => + h(Suspense, null, { + default: () => + h( + Fragment, + null, + items.value.map(item => + h(CompAsyncSetup, { item, key: item.id }), + ), + ), + }) + }, + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + await nextTick() + await Promise.all(deps) + + expect(serializeInner(root)).toBe( + `
111
222
333
`, + ) + + items.value = [ + { id: 4, name: '444' }, + { id: 5, name: '555' }, + { id: 6, name: '666' }, + ] + await nextTick() + await Promise.all(deps) + expect(serializeInner(root)).toBe( + `
444
555
666
`, + ) + }) }) }) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index a57be791a44..f046e93ad85 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1226,6 +1226,7 @@ function baseCreateRenderer( if (!initialVNode.el) { const placeholder = (instance.subTree = createVNode(Comment)) processCommentNode(null, placeholder, container!, anchor) + initialVNode.placeholder = placeholder.el } } else { setupRenderEffect( @@ -1979,8 +1980,12 @@ function baseCreateRenderer( for (i = toBePatched - 1; i >= 0; i--) { const nextIndex = s2 + i const nextChild = c2[nextIndex] as VNode + const anchorVNode = c2[nextIndex + 1] as VNode const anchor = - nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor + nextIndex + 1 < l2 + ? // #13559, fallback to el placeholder for unresolved async component + anchorVNode.el || anchorVNode.placeholder + : parentAnchor if (newIndexToOldIndexMap[i] === 0) { // mount new patch( diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index a8c5340cd1f..cd1ef948d73 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -196,6 +196,7 @@ export interface VNode< // DOM el: HostNode | null + placeholder: HostNode | null // async component el placeholder anchor: HostNode | null // fragment anchor target: HostElement | null // teleport target targetStart: HostNode | null // teleport target start anchor @@ -711,6 +712,8 @@ export function cloneVNode( suspense: vnode.suspense, ssContent: vnode.ssContent && cloneVNode(vnode.ssContent), ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback), + placeholder: vnode.placeholder, + el: vnode.el, anchor: vnode.anchor, ctx: vnode.ctx,