From 9f48e7620f5bf017a04b2a57af82b839a4e8f496 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:34:38 +0100 Subject: [PATCH] fix: repair dynamic component truthy/falsy hydration mismatches (#17737) Fixes #17735 Use the if/else hydration markers to know what "branch" (component or no component) was rendered, and repair if differing. ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [x] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` --- .changeset/silly-mammals-fold.md | 5 +++ .../server/visitors/shared/component.js | 44 +++++++++++++------ .../client/dom/blocks/svelte-component.js | 37 +++++++++++++++- packages/svelte/src/internal/server/index.js | 10 ++--- .../Component.svelte | 7 +++ .../dynamic-component-css-props/_config.js | 10 +++++ .../dynamic-component-css-props/main.svelte | 6 +++ .../HelloWorld.svelte | 1 + .../_config.js | 9 ++++ .../main.svelte | 13 ++++++ 10 files changed, 123 insertions(+), 19 deletions(-) create mode 100644 .changeset/silly-mammals-fold.md create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/HelloWorld.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/main.svelte diff --git a/.changeset/silly-mammals-fold.md b/.changeset/silly-mammals-fold.md new file mode 100644 index 0000000000..3513b4339e --- /dev/null +++ b/.changeset/silly-mammals-fold.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: repair dynamic component truthy/falsy hydration mismatches diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index 6a2c6eb0be..b0e086b7ad 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -1,7 +1,14 @@ /** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../../types.js' */ -import { empty_comment, build_attribute_value, PromiseOptimiser } from './utils.js'; +import { + empty_comment, + build_attribute_value, + PromiseOptimiser, + block_open_else, + block_open, + block_close +} from './utils.js'; import * as b from '#compiler/builders'; import { is_element_node } from '../../../../nodes.js'; import { dev } from '../../../../../state.js'; @@ -300,9 +307,22 @@ export function build_inline_component(node, expression, context) { node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic); /** @type {Statement} */ - let statement = b.stmt( - (dynamic ? b.maybe_call : b.call)(expression, b.id('$$renderer'), props_expression) - ); + let statement = b.stmt(b.call(expression, b.id('$$renderer'), props_expression)); + + if (dynamic) { + statement = b.if( + expression, + b.block([ + b.stmt(b.call('$$renderer.push', block_open)), + statement, + b.stmt(b.call('$$renderer.push', block_close)) + ]), + b.block([ + b.stmt(b.call('$$renderer.push', block_open_else)), + b.stmt(b.call('$$renderer.push', block_close)) + ]) + ); + } if (snippet_declarations.length > 0) { statement = b.block([...snippet_declarations, statement]); @@ -326,16 +346,14 @@ export function build_inline_component(node, expression, context) { optimiser.check_blockers(node.metadata.expression); } - context.state.template.push( - ...optimiser.render_block([ - dynamic && custom_css_props.length === 0 - ? b.stmt(b.call('$$renderer.push', empty_comment)) - : b.empty, - statement - ]) - ); + context.state.template.push(...optimiser.render_block([statement])); - if (!optimiser.is_async() && !context.state.is_standalone && custom_css_props.length === 0) { + if ( + !dynamic && + !optimiser.is_async() && + !context.state.is_standalone && + custom_css_props.length === 0 + ) { context.state.template.push(empty_comment); } } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js index 134e57e627..0e9434386c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,8 +1,17 @@ /** @import { TemplateNode, Dom } from '#client' */ import { EFFECT_TRANSPARENT } from '#client/constants'; import { block } from '../../reactivity/effects.js'; -import { hydrate_next, hydrating } from '../hydration.js'; +import { + hydrate_next, + hydrate_node, + hydrating, + read_hydration_instruction, + set_hydrate_node, + set_hydrating, + skip_nodes +} from '../hydration.js'; import { BranchManager } from './branches.js'; +import { HYDRATION_START, HYDRATION_START_ELSE } from '../../../../constants.js'; /** * @template P @@ -13,7 +22,11 @@ import { BranchManager } from './branches.js'; * @returns {void} */ export function component(node, get_component, render_fn) { + /** @type {TemplateNode | undefined} */ + var hydration_start_node; + if (hydrating) { + hydration_start_node = hydrate_node; hydrate_next(); } @@ -21,6 +34,28 @@ export function component(node, get_component, render_fn) { block(() => { var component = get_component() ?? null; + + if (hydrating) { + var data = read_hydration_instruction(/** @type {TemplateNode} */ (hydration_start_node)); + + var server_had_component = data === HYDRATION_START; + var client_has_component = component !== null; + + if (server_had_component !== client_has_component) { + // Hydration mismatch: skip the server-rendered nodes and render fresh + var anchor = skip_nodes(); + + set_hydrate_node(anchor); + branches.anchor = anchor; + + set_hydrating(false); + branches.ensure(component, component && ((target) => render_fn(target, component))); + set_hydrating(true); + + return; + } + } + branches.ensure(component, component && ((target) => render_fn(target, component))); }, EFFECT_TRANSPARENT); } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 0864c7704d..6ab32a0d6e 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -105,16 +105,16 @@ export function css_props(renderer, is_html, props, component, dynamic = false) renderer.push(``); } - if (dynamic) { + component(); + + if (!dynamic) { renderer.push(''); } - component(); - if (is_html) { - renderer.push(``); + renderer.push(''); } else { - renderer.push(``); + renderer.push(''); } } diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/Component.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/Component.svelte new file mode 100644 index 0000000000..57add17179 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/Component.svelte @@ -0,0 +1,7 @@ +
Hello
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/_config.js new file mode 100644 index 0000000000..3078392055 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual( + target.innerHTML, + `
Hello
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/main.svelte new file mode 100644 index 0000000000..80aec26eef --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/main.svelte @@ -0,0 +1,6 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/HelloWorld.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/HelloWorld.svelte new file mode 100644 index 0000000000..52ea02c559 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/HelloWorld.svelte @@ -0,0 +1 @@ +
Hello world
diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/_config.js new file mode 100644 index 0000000000..90dae38ad8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/_config.js @@ -0,0 +1,9 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, `

Test

Hello world
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/main.svelte new file mode 100644 index 0000000000..8a410da609 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/main.svelte @@ -0,0 +1,13 @@ + + +

Test

+ +