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`
pull/17743/head
Simon H 6 days ago committed by GitHub
parent 8ea33bf7fe
commit 9f48e7620f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: repair dynamic component truthy/falsy hydration mismatches

@ -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);
}
}

@ -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);
}

@ -105,16 +105,16 @@ export function css_props(renderer, is_html, props, component, dynamic = false)
renderer.push(`<g style="${styles}">`);
}
if (dynamic) {
component();
if (!dynamic) {
renderer.push('<!---->');
}
component();
if (is_html) {
renderer.push(`<!----></svelte-css-wrapper>`);
renderer.push('</svelte-css-wrapper>');
} else {
renderer.push(`<!----></g>`);
renderer.push('</g>');
}
}

@ -0,0 +1,7 @@
<div>Hello</div>
<style>
div {
color: var(--color);
}
</style>

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
assert.htmlEqual(
target.innerHTML,
`<svelte-css-wrapper style="display: contents; --color: red;"><div class="svelte-lsmn3l">Hello</div></svelte-css-wrapper>`
);
}
});

@ -0,0 +1,6 @@
<script>
import Component from './Component.svelte';
let Comp = Component;
</script>
<svelte:component this={Comp} --color="red" />

@ -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, `<h1>Test</h1> <div>Hello world</div>`);
}
});

@ -0,0 +1,13 @@
<script>
import HelloWorld from './HelloWorld.svelte';
let Component = $state();
$effect.pre(() => {
Component = HelloWorld;
});
</script>
<h1>Test</h1>
<Component />
Loading…
Cancel
Save