Skip to content

Commit f118f8e

Browse files
trueadmRich-Harris
andauthored
fix: improve action support for nested $effect (#10962)
* fix: improve action support for nested $effect * tweaks * simplify * comment --------- Co-authored-by: Rich Harris <[email protected]>
1 parent d50b766 commit f118f8e

File tree

6 files changed

+111
-38
lines changed

6 files changed

+111
-38
lines changed

.changeset/itchy-bulldogs-tan.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: improve action support for nested $effect
Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,37 @@
1-
import { effect } from '../../reactivity/effects.js';
1+
import { effect, render_effect } from '../../reactivity/effects.js';
22
import { deep_read_state, untrack } from '../../runtime.js';
33

44
/**
55
* @template P
66
* @param {Element} dom
77
* @param {(dom: Element, value?: P) => import('#client').ActionPayload<P>} action
8-
* @param {() => P} [value_fn]
8+
* @param {() => P} [get_value]
99
* @returns {void}
1010
*/
11-
export function action(dom, action, value_fn) {
12-
/** @type {undefined | import('#client').ActionPayload<P>} */
13-
var payload = undefined;
14-
var needs_deep_read = false;
15-
16-
// Action could come from a prop, therefore could be a signal, therefore untrack
17-
// TODO we could take advantage of this and enable https://github.com/sveltejs/svelte/issues/6942
11+
export function action(dom, action, get_value) {
1812
effect(() => {
19-
if (value_fn) {
20-
var value = value_fn();
21-
untrack(() => {
22-
if (payload === undefined) {
23-
payload = action(dom, value) || {};
24-
needs_deep_read = !!payload?.update;
25-
} else {
26-
var update = payload.update;
27-
if (typeof update === 'function') {
28-
update(value);
29-
}
13+
var payload = untrack(() => action(dom, get_value?.()) || {});
14+
var update = payload?.update;
15+
16+
if (get_value && update) {
17+
var inited = false;
18+
19+
render_effect(() => {
20+
var value = get_value();
21+
22+
// Action's update method is coarse-grained, i.e. when anything in the passed value changes, update.
23+
// This works in legacy mode because of mutable_source being updated as a whole, but when using $state
24+
// together with actions and mutation, it wouldn't notice the change without a deep read.
25+
deep_read_state(value);
26+
27+
if (inited) {
28+
/** @type {Function} */ (update)(value);
3029
}
3130
});
32-
// Action's update method is coarse-grained, i.e. when anything in the passed value changes, update.
33-
// This works in legacy mode because of mutable_source being updated as a whole, but when using $state
34-
// together with actions and mutation, it wouldn't notice the change without a deep read.
35-
if (needs_deep_read) {
36-
deep_read_state(value);
37-
}
38-
} else {
39-
untrack(() => (payload = action(dom)));
40-
}
41-
});
4231

43-
effect(() => {
44-
if (payload !== undefined) {
45-
var destroy = payload.destroy;
46-
if (typeof destroy === 'function') {
47-
return () => {
48-
/** @type {Function} */ (destroy)();
49-
};
50-
}
32+
inited = true;
5133
}
34+
35+
return payload?.destroy;
5236
});
5337
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script>
2+
import { log } from './log.js';
3+
4+
const { prop } = $props()
5+
6+
let trackedState = $state(0)
7+
const getTrackedState = () => trackedState
8+
9+
function dummyAction(el, { getTrackedState, propFromComponent }) {
10+
$effect(() => {
11+
log.push("action $effect: ", { buttonClicked: getTrackedState() })
12+
})
13+
}
14+
</script>
15+
16+
<div
17+
class="container"
18+
use:dummyAction={{ getTrackedState, propFromComponent: prop }}
19+
>
20+
{JSON.stringify(prop)}
21+
</div>
22+
23+
<button
24+
onclick={() => {
25+
trackedState += 1
26+
}}
27+
>
28+
update tracked state
29+
</button>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { test } from '../../test';
2+
import { flushSync, tick } from 'svelte';
3+
import { log } from './log.js';
4+
5+
export default test({
6+
before_test() {
7+
log.length = 0;
8+
},
9+
10+
html: `<div class="container">{"text":"initial"}</div><button>update tracked state</button><button>Update prop</button>`,
11+
12+
async test({ assert, target }) {
13+
const [btn1, btn2] = target.querySelectorAll('button');
14+
15+
flushSync(() => {
16+
btn2.click();
17+
});
18+
19+
assert.htmlEqual(
20+
target.innerHTML,
21+
`<div class="container">{"text":"updated"}</div><button>update tracked state</button><button>Update prop</button>`
22+
);
23+
24+
flushSync(() => {
25+
btn1.click();
26+
});
27+
28+
assert.htmlEqual(
29+
target.innerHTML,
30+
`<div class="container">{"text":"updated"}</div><button>update tracked state</button><button>Update prop</button>`
31+
);
32+
33+
assert.deepEqual(log, [
34+
'action $effect: ',
35+
{ buttonClicked: 0 },
36+
'action $effect: ',
37+
{ buttonClicked: 1 }
38+
]);
39+
}
40+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/** @type {any[]} */
2+
export const log = [];
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
import Task from "./Task.svelte"
3+
4+
let task = $state({ text: "initial" })
5+
</script>
6+
7+
<Task prop={task} />
8+
9+
<button onclick={() => {
10+
task = { text: "updated" }
11+
}}>
12+
Update prop
13+
</button>

0 commit comments

Comments
 (0)