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,