diff --git a/.changeset/itchy-bulldogs-tan.md b/.changeset/itchy-bulldogs-tan.md new file mode 100644 index 0000000000..94d5922ec7 --- /dev/null +++ b/.changeset/itchy-bulldogs-tan.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve action support for nested $effect diff --git a/packages/svelte/src/internal/client/dom/elements/actions.js b/packages/svelte/src/internal/client/dom/elements/actions.js index e6de87e78f..669a14c057 100644 --- a/packages/svelte/src/internal/client/dom/elements/actions.js +++ b/packages/svelte/src/internal/client/dom/elements/actions.js @@ -1,53 +1,37 @@ -import { effect } from '../../reactivity/effects.js'; +import { effect, render_effect } from '../../reactivity/effects.js'; import { deep_read_state, untrack } from '../../runtime.js'; /** * @template P * @param {Element} dom * @param {(dom: Element, value?: P) => import('#client').ActionPayload

} action - * @param {() => P} [value_fn] + * @param {() => P} [get_value] * @returns {void} */ -export function action(dom, action, value_fn) { - /** @type {undefined | import('#client').ActionPayload

} */ - var payload = undefined; - var needs_deep_read = false; - - // Action could come from a prop, therefore could be a signal, therefore untrack - // TODO we could take advantage of this and enable https://github.com/sveltejs/svelte/issues/6942 +export function action(dom, action, get_value) { effect(() => { - if (value_fn) { - var value = value_fn(); - untrack(() => { - if (payload === undefined) { - payload = action(dom, value) || {}; - needs_deep_read = !!payload?.update; - } else { - var update = payload.update; - if (typeof update === 'function') { - update(value); - } + var payload = untrack(() => action(dom, get_value?.()) || {}); + var update = payload?.update; + + if (get_value && update) { + var inited = false; + + render_effect(() => { + var value = get_value(); + + // Action's update method is coarse-grained, i.e. when anything in the passed value changes, update. + // This works in legacy mode because of mutable_source being updated as a whole, but when using $state + // together with actions and mutation, it wouldn't notice the change without a deep read. + deep_read_state(value); + + if (inited) { + /** @type {Function} */ (update)(value); } }); - // Action's update method is coarse-grained, i.e. when anything in the passed value changes, update. - // This works in legacy mode because of mutable_source being updated as a whole, but when using $state - // together with actions and mutation, it wouldn't notice the change without a deep read. - if (needs_deep_read) { - deep_read_state(value); - } - } else { - untrack(() => (payload = action(dom))); - } - }); - effect(() => { - if (payload !== undefined) { - var destroy = payload.destroy; - if (typeof destroy === 'function') { - return () => { - /** @type {Function} */ (destroy)(); - }; - } + inited = true; } + + return payload?.destroy; }); } diff --git a/packages/svelte/tests/runtime-runes/samples/action-state-arg-deep/Task.svelte b/packages/svelte/tests/runtime-runes/samples/action-state-arg-deep/Task.svelte new file mode 100644 index 0000000000..c6087dc822 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/action-state-arg-deep/Task.svelte @@ -0,0 +1,29 @@ + + +

+ {JSON.stringify(prop)} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/action-state-arg-deep/_config.js b/packages/svelte/tests/runtime-runes/samples/action-state-arg-deep/_config.js new file mode 100644 index 0000000000..c263855bec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/action-state-arg-deep/_config.js @@ -0,0 +1,40 @@ +import { test } from '../../test'; +import { flushSync, tick } from 'svelte'; +import { log } from './log.js'; + +export default test({ + before_test() { + log.length = 0; + }, + + html: `
{"text":"initial"}
`, + + async test({ assert, target }) { + const [btn1, btn2] = target.querySelectorAll('button'); + + flushSync(() => { + btn2.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
{"text":"updated"}
` + ); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
{"text":"updated"}
` + ); + + assert.deepEqual(log, [ + 'action $effect: ', + { buttonClicked: 0 }, + 'action $effect: ', + { buttonClicked: 1 } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/action-state-arg-deep/log.js b/packages/svelte/tests/runtime-runes/samples/action-state-arg-deep/log.js new file mode 100644 index 0000000000..d3df521f4d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/action-state-arg-deep/log.js @@ -0,0 +1,2 @@ +/** @type {any[]} */ +export const log = []; diff --git a/packages/svelte/tests/runtime-runes/samples/action-state-arg-deep/main.svelte b/packages/svelte/tests/runtime-runes/samples/action-state-arg-deep/main.svelte new file mode 100644 index 0000000000..b889ef09f7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/action-state-arg-deep/main.svelte @@ -0,0 +1,13 @@ + + + + +