diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 6caa2442e18..e0277622c13 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -7,7 +7,10 @@ import { Teleport, Transition, type VNode, + createBlock, createCommentVNode, + createElementBlock, + createElementVNode, createSSRApp, createStaticVNode, createTextVNode, @@ -17,16 +20,19 @@ import { h, nextTick, onMounted, + openBlock, ref, renderSlot, useCssVars, vModelCheckbox, vShow, + withCtx, withDirectives, } from '@vue/runtime-dom' import { type SSRContext, renderToString } from '@vue/server-renderer' import { PatchFlags } from '@vue/shared' import { vShowOriginalDisplay } from '../../runtime-dom/src/directives/vShow' +import { expect } from 'vitest' function mountWithHydration(html: string, render: () => any) { const container = document.createElement('div') @@ -1292,6 +1298,81 @@ describe('SSR hydration', () => { `) }) + // #10607 + test('update component stable slot (prod + optimized mode)', async () => { + __DEV__ = false + const container = document.createElement('div') + container.innerHTML = `` + const Comp = { + render(this: any) { + return ( + openBlock(), + createElementBlock('div', null, [renderSlot(this.$slots, 'default')]) + ) + }, + } + const show = ref(false) + const clicked = ref(false) + + const Wrapper = { + setup() { + const items = ref([]) + onMounted(() => { + items.value = [1] + }) + return () => { + return ( + openBlock(), + createBlock(Comp, null, { + default: withCtx(() => [ + createElementVNode('div', null, [ + createElementVNode('div', null, [ + clicked.value + ? (openBlock(), + createElementBlock('div', { key: 0 }, 'foo')) + : createCommentVNode('v-if', true), + ]), + ]), + createElementVNode( + 'div', + null, + items.value.length, + 1 /* TEXT */, + ), + ]), + _: 1 /* STABLE */, + }) + ) + } + }, + } + createSSRApp({ + components: { Wrapper }, + data() { + return { show } + }, + template: ``, + }).mount(container) + + await nextTick() + expect(container.innerHTML).toBe( + `
1
`, + ) + + show.value = true + await nextTick() + expect(async () => { + clicked.value = true + await nextTick() + }).not.toThrow("Cannot read properties of null (reading 'insertBefore')") + + await nextTick() + expect(container.innerHTML).toBe( + `
foo
1
`, + ) + __DEV__ = true + }) + describe('mismatch handling', () => { test('text node', () => { const { container } = mountWithHydration(`foo`, () => 'bar') diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 8469577608e..a7832ac3d57 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -120,6 +120,7 @@ export function createHydrationFunctions( slotScopeIds: string[] | null, optimized = false, ): Node | null => { + optimized = optimized || !!vnode.dynamicChildren const isFragmentStart = isComment(node) && node.data === '[' const onMismatch = () => handleMismatch(