fix: improve action support for nested $effect (#10962)

* fix: improve action support for nested $effect

* tweaks

* simplify

* comment

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/10963/head
Dominic Gannaway 1 year ago committed by GitHub
parent d50b7661e5
commit f118f8ea27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve action support for nested $effect

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

@ -0,0 +1,29 @@
<script>
import { log } from './log.js';
const { prop } = $props()
let trackedState = $state(0)
const getTrackedState = () => trackedState
function dummyAction(el, { getTrackedState, propFromComponent }) {
$effect(() => {
log.push("action $effect: ", { buttonClicked: getTrackedState() })
})
}
</script>
<div
class="container"
use:dummyAction={{ getTrackedState, propFromComponent: prop }}
>
{JSON.stringify(prop)}
</div>
<button
onclick={() => {
trackedState += 1
}}
>
update tracked state
</button>

@ -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: `<div class="container">{"text":"initial"}</div><button>update tracked state</button><button>Update prop</button>`,
async test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');
flushSync(() => {
btn2.click();
});
assert.htmlEqual(
target.innerHTML,
`<div class="container">{"text":"updated"}</div><button>update tracked state</button><button>Update prop</button>`
);
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<div class="container">{"text":"updated"}</div><button>update tracked state</button><button>Update prop</button>`
);
assert.deepEqual(log, [
'action $effect: ',
{ buttonClicked: 0 },
'action $effect: ',
{ buttonClicked: 1 }
]);
}
});

@ -0,0 +1,2 @@
/** @type {any[]} */
export const log = [];

@ -0,0 +1,13 @@
<script>
import Task from "./Task.svelte"
let task = $state({ text: "initial" })
</script>
<Task prop={task} />
<button onclick={() => {
task = { text: "updated" }
}}>
Update prop
</button>
Loading…
Cancel
Save