diff --git a/packages/runtime-core/__tests__/rendererFragment.spec.ts b/packages/runtime-core/__tests__/rendererFragment.spec.ts
index d0c9e5c79a7..fbd9af11857 100644
--- a/packages/runtime-core/__tests__/rendererFragment.spec.ts
+++ b/packages/runtime-core/__tests__/rendererFragment.spec.ts
@@ -13,9 +13,13 @@ import {
createTextVNode,
createBlock,
openBlock,
- createCommentVNode
+ createCommentVNode,
+ withCtx,
+ createElementBlock,
+ ref,
+ renderSlot
} from '@vue/runtime-test'
-import { PatchFlags } from '@vue/shared'
+import { PatchFlags, SlotFlags } from '@vue/shared'
import { renderList } from '../src/helpers/renderList'
describe('renderer: fragment', () => {
@@ -351,4 +355,121 @@ describe('renderer: fragment', () => {
render(renderFn(['two', 'one']), root)
expect(serializeInner(root)).toBe(`text
two
textone
`)
})
+
+ // #9200
+ test('stable fragment in unstable slot', () => {
+ const root = nodeOps.createElement('div')
+
+ const items = ref([{ field1: 'one', field2: 'two' as string | undefined }])
+
+ const textBlock = 'text-block'
+ const vIfBlock = 'v-if-block'
+
+ const Comp = {
+ render(ctx: any) {
+ return (
+ openBlock(true),
+ createElementBlock(
+ Fragment,
+ null,
+ renderList(items.value, (item, i) => {
+ return (
+ openBlock(),
+ createElementBlock('div', { key: i }, [
+ (openBlock(true),
+ createElementBlock(
+ Fragment,
+ null,
+ renderList(['field1', 'field2'] as const, field => {
+ return (
+ openBlock(),
+ createElementBlock('span', { key: field }, [
+ renderSlot(
+ ctx.$slots,
+ 'default',
+ {
+ value: item[field],
+ field: field
+ },
+ () => [
+ item[field]
+ ? (openBlock(),
+ createElementBlock(
+ Fragment,
+ { key: 0 },
+ [createTextVNode('xxx')],
+ PatchFlags.STABLE_FRAGMENT
+ ))
+ : (openBlock(),
+ createElementBlock(
+ Fragment,
+ { key: 1 },
+ [createTextVNode('yyy')],
+ PatchFlags.STABLE_FRAGMENT
+ ))
+ ]
+ )
+ ])
+ )
+ }),
+ PatchFlags.KEYED_FRAGMENT
+ ))
+ ])
+ )
+ }),
+ PatchFlags.KEYED_FRAGMENT
+ )
+ )
+ }
+ }
+
+ const hoisted1 = { key: 0 }
+ const hoisted2 = { key: 0 }
+ const hoisted3 = { key: 1 }
+
+ const renderFn = () => {
+ return (
+ openBlock(true),
+ createVNode(Comp, null, {
+ default: withCtx(
+ ({ field, value }: { field: string; value: any }) => [
+ field === 'field1'
+ ? (openBlock(), createElementBlock('div', hoisted1, textBlock))
+ : field === 'field2'
+ ? (openBlock(),
+ createElementBlock(
+ Fragment,
+ { key: 1 },
+ [
+ value
+ ? (openBlock(),
+ createElementBlock('div', hoisted2, vIfBlock))
+ : createCommentVNode('v-if', true),
+ value
+ ? (openBlock(),
+ createElementBlock('div', hoisted3, vIfBlock))
+ : createCommentVNode('v-if', true)
+ ],
+ PatchFlags.STABLE_FRAGMENT
+ ))
+ : createCommentVNode('v-if', true)
+ ]
+ ),
+ _: SlotFlags.STABLE
+ })
+ )
+ }
+
+ render(renderFn(), root)
+ expect(serializeInner(root)).toBe(
+ `${textBlock}
${vIfBlock}
${vIfBlock}
`
+ )
+
+ items.value = [{ field1: 'one', field2: undefined }]
+
+ render(renderFn(), root)
+ expect(serializeInner(root)).toBe(
+ ``
+ )
+ })
})
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index 383e17fb0f5..25ca065e85a 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -1098,7 +1098,9 @@ function baseCreateRenderer(
dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children
- n1.dynamicChildren
+ n1.dynamicChildren &&
+ // #9200 in some case stable fragment in deep unstable slot
+ n1.dynamicChildren.length === dynamicChildren.length
) {
// a stable fragment (template root or ) doesn't need to
// patch children order, but it may contain dynamicChildren.