Skip to content

Commit f1d3c68

Browse files
committed
fix: prevent reactive snippet from reinitializing unnecessarily
untrack the invocation itself, only track the snippet function fixes #9652
1 parent 405e9da commit f1d3c68

File tree

6 files changed

+112
-10
lines changed

6 files changed

+112
-10
lines changed

.changeset/light-pens-watch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: prevent reactive snippet from reinitializing unnecessarily

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,18 +1734,20 @@ export const template_visitors = {
17341734
if (node.argument) {
17351735
args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.argument))));
17361736
}
1737-
const snippet_function = /** @type {import('estree').Expression} */ (
1737+
1738+
let snippet_function = /** @type {import('estree').Expression} */ (
17381739
context.visit(node.expression)
17391740
);
1740-
const init = b.call(
1741-
context.state.options.dev ? b.call('$.validate_snippet', snippet_function) : snippet_function,
1742-
...args
1743-
);
1741+
if (context.state.options.dev) {
1742+
snippet_function = b.call('$.validate_snippet', snippet_function);
1743+
}
17441744

17451745
if (is_reactive) {
1746-
context.state.init.push(b.stmt(b.call('$.snippet_effect', b.thunk(init))));
1746+
context.state.init.push(
1747+
b.stmt(b.call('$.snippet_effect', b.thunk(snippet_function), ...args))
1748+
);
17471749
} else {
1748-
context.state.init.push(b.stmt(init));
1750+
context.state.init.push(b.stmt(b.call(snippet_function, ...args)));
17491751
}
17501752
},
17511753
AnimateDirective(node, { state, visit }) {

packages/svelte/src/internal/client/render.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3170,13 +3170,18 @@ export function sanitize_slots(props) {
31703170
}
31713171

31723172
/**
3173-
* @param {() => void} create_snippet
3173+
* @param {() => Function} get_snippet
3174+
* @param {Node} node
3175+
* @param {() => any} args
31743176
* @returns {void}
31753177
*/
3176-
export function snippet_effect(create_snippet) {
3178+
export function snippet_effect(get_snippet, node, args) {
31773179
const block = create_snippet_block();
31783180
render_effect(() => {
3179-
create_snippet();
3181+
// Only rerender when the snippet function itself changes,
3182+
// not when an eagerly-read prop inside the snippet function changes
3183+
const snippet = get_snippet();
3184+
untrack(() => snippet(node, args));
31803185
return () => {
31813186
if (block.d !== null) {
31823187
remove(block.d);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `
5+
<p>snippet: 0</p>
6+
<button>toggle</button>
7+
<button>increase count</button>
8+
`,
9+
props: {
10+
get log() {
11+
return [];
12+
}
13+
},
14+
15+
async test({ assert, target, component }) {
16+
const [toggle, increment] = target.querySelectorAll('button');
17+
18+
await increment?.click();
19+
assert.htmlEqual(
20+
target.innerHTML,
21+
`
22+
<p>snippet: 1</p>
23+
<button>toggle</button>
24+
<button>increase count</button>
25+
`
26+
);
27+
assert.deepEqual(component.log, []);
28+
29+
await toggle?.click();
30+
assert.htmlEqual(
31+
target.innerHTML,
32+
`
33+
<p>component: 1</p>
34+
<button>toggle</button>
35+
<button>increase count</button>
36+
`
37+
);
38+
assert.deepEqual(component.log, [1]);
39+
40+
await increment?.click();
41+
assert.htmlEqual(
42+
target.innerHTML,
43+
`
44+
<p>component: 2</p>
45+
<button>toggle</button>
46+
<button>increase count</button>
47+
`
48+
);
49+
assert.deepEqual(component.log, [1]);
50+
51+
await toggle?.click();
52+
assert.htmlEqual(
53+
target.innerHTML,
54+
`
55+
<p>snippet: 2</p>
56+
<button>toggle</button>
57+
<button>increase count</button>
58+
`
59+
);
60+
assert.deepEqual(component.log, [1]);
61+
}
62+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
let { count, log } = $props();
3+
log.push(count);
4+
</script>
5+
6+
<p>component: {count}</p>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script>
2+
import Inner from "./inner.svelte";
3+
4+
let { log } = $props();
5+
6+
let count = $state(0);
7+
let show_foo = $state(true);
8+
let snippet = $derived(show_foo ? foo : bar);
9+
</script>
10+
11+
{#snippet foo({count})}
12+
<p>snippet: {count}</p>
13+
{/snippet}
14+
15+
{#snippet bar(props)}
16+
<Inner {...props}></Inner>
17+
{/snippet}
18+
19+
{@render snippet({ count, log })}
20+
21+
<button onclick={() => show_foo = !show_foo}>toggle</button>
22+
<button onclick={() => count++}>increase count</button>

0 commit comments

Comments
 (0)