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(
+ ``,
+ )
+
+ 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(
+ ``,
+ )
+ __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(