fix: prevent reactive snippet from reinitializing unnecessarily (#9665)

untrack the invocation itself, only track the snippet function
fixes #9652
pull/9667/head
Simon H 1 year ago committed by GitHub
parent 5619cd9bfc
commit 83fd001157
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent reactive snippet from reinitializing unnecessarily

@ -1734,18 +1734,20 @@ export const template_visitors = {
if (node.argument) {
args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.argument))));
}
const snippet_function = /** @type {import('estree').Expression} */ (
let snippet_function = /** @type {import('estree').Expression} */ (
context.visit(node.expression)
);
const init = b.call(
context.state.options.dev ? b.call('$.validate_snippet', snippet_function) : snippet_function,
...args
);
if (context.state.options.dev) {
snippet_function = b.call('$.validate_snippet', snippet_function);
}
if (is_reactive) {
context.state.init.push(b.stmt(b.call('$.snippet_effect', b.thunk(init))));
context.state.init.push(
b.stmt(b.call('$.snippet_effect', b.thunk(snippet_function), ...args))
);
} else {
context.state.init.push(b.stmt(init));
context.state.init.push(b.stmt(b.call(snippet_function, ...args)));
}
},
AnimateDirective(node, { state, visit }) {

@ -3170,13 +3170,18 @@ export function sanitize_slots(props) {
}
/**
* @param {() => void} create_snippet
* @param {() => Function} get_snippet
* @param {Node} node
* @param {() => any} args
* @returns {void}
*/
export function snippet_effect(create_snippet) {
export function snippet_effect(get_snippet, node, args) {
const block = create_snippet_block();
render_effect(() => {
create_snippet();
// Only rerender when the snippet function itself changes,
// not when an eagerly-read prop inside the snippet function changes
const snippet = get_snippet();
untrack(() => snippet(node, args));
return () => {
if (block.d !== null) {
remove(block.d);

@ -0,0 +1,62 @@
import { test } from '../../test';
export default test({
html: `
<p>snippet: 0</p>
<button>toggle</button>
<button>increase count</button>
`,
props: {
get log() {
return [];
}
},
async test({ assert, target, component }) {
const [toggle, increment] = target.querySelectorAll('button');
await increment?.click();
assert.htmlEqual(
target.innerHTML,
`
<p>snippet: 1</p>
<button>toggle</button>
<button>increase count</button>
`
);
assert.deepEqual(component.log, []);
await toggle?.click();
assert.htmlEqual(
target.innerHTML,
`
<p>component: 1</p>
<button>toggle</button>
<button>increase count</button>
`
);
assert.deepEqual(component.log, [1]);
await increment?.click();
assert.htmlEqual(
target.innerHTML,
`
<p>component: 2</p>
<button>toggle</button>
<button>increase count</button>
`
);
assert.deepEqual(component.log, [1]);
await toggle?.click();
assert.htmlEqual(
target.innerHTML,
`
<p>snippet: 2</p>
<button>toggle</button>
<button>increase count</button>
`
);
assert.deepEqual(component.log, [1]);
}
});

@ -0,0 +1,6 @@
<script>
let { count, log } = $props();
log.push(count);
</script>
<p>component: {count}</p>

@ -0,0 +1,22 @@
<script>
import Inner from "./inner.svelte";
let { log } = $props();
let count = $state(0);
let show_foo = $state(true);
let snippet = $derived(show_foo ? foo : bar);
</script>
{#snippet foo({count})}
<p>snippet: {count}</p>
{/snippet}
{#snippet bar(props)}
<Inner {...props}></Inner>
{/snippet}
{@render snippet({ count, log })}
<button onclick={() => show_foo = !show_foo}>toggle</button>
<button onclick={() => count++}>increase count</button>
Loading…
Cancel
Save