-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow opt-in explicit dependency tracking for $effect
#9248
Comments
You can implement this in user land: function explicitEffect(fn, depsFn) {
$effect(() => {
depsFn();
untrack(fn);
});
}
// usage
explicitEffect(
() => {
// do stuff
},
() => [dep1, dep2, dep3]
); |
I use a similar pattern when e.g. a prop change, this would also benifit from being able to opt in on what to track instead of remembering to untrack everything. export let data;
let a;
let b;
$: init(data);
function init(data) {
a = data.x.filter(...);
b = data.y.toSorted();
} I'm only interrested in calling |
Good approach. Don't love the syntax, but I suppose it's workable. |
I think you want const { data } = $props();
const a = $derived(data.x.filter(...));
const b = $derived(data.y.toSorted()); |
Thanks @ottomated, that seems reasonable. The inverse of $track([dep1, dep2], () => {
// do stuff
}); But let's see what happens 🙂 |
FWIW I currently make stuff update by passing in parameter into a reactive function call eg <script>
let c = 0, a = 0, b = 0
function update(){
c = a+b
}
S: update(a,b)
</sctipt> Perhaps effect could take parameters on the things to watch, defaulting to all $effect((a,b) => {
c = a + b
}); That's got to be better that the 'untrack' example here https://svelte-5-preview.vercel.app/docs/functions#untrack Also I think the $watch((a,b) => {
c = a + b
}); |
@crisward I personally love the syntax you came up with, but I'm sure there's gonna be a lot of complaints that "that's not how javascript actually / normally works". |
It could cause issues/confusion when intentionally using recursive effects that assign one of the dependencies. A side note on the names: I am in favor of having explicit tracking, maybe even as the recommended/intended default; as of now the potential for errors with |
I'd opt for using a config object over an array if this were to be implemented. $effect(() => {
timesChanged++;
}, {
track: [num],
}); More explicit, and makes it more future-proof to other config that might potentially be wanted. e.g. there could be a desire for an |
But that would mean that we cant pass callbacks like it was intended before. function recalc() {
c = a + b
}
$effect(recalc) Im not really in favor of |
The API shapes I'd propose: $effect.on(() => {
// do stuff...
}, dep1, dep2, dep3); Or $watch([dep1, dep2, dep3], () => {
// do stuff
}); I actually prefer With Plus, the word |
I think something like this would be pretty useful. Usually when I needed to manually control which variables triggered reactivity it was easier for me to think about which variables to track, and not which ones to |
I'd be in favor of a let { id, name, description, location, tags, contents } = $state(data.container);
let saveState = $state<'saved' | 'saving' | 'dirty'>('saved');
$effect(() => {
[id, name, description, location, ...tags, ...contents];
untrack(() => {
saveState = 'dirty';
});
}); I'm using the array to reference all the dependencies. The tags and contents both need to be spread so that the effect triggers when the individual array items change. Then I need to wrap $watch([id, name, description, location, ...tags, ...contents], () => {
saveState = 'dirty';
}) I think it makes it more obvious why the array is being used, and it simplifies the callback function since |
@eddiemcconkie that's a great example of a good opportunity to explicitly define dependencies we could also add an options parameter like this $effect(() => saveState = 'dirty', { track: [id, name, description, location, ...tags, ...contents] }) which I think has the advantage of reflecting that it really does the same as a regular |
@opensas would that still track dependencies based on what's referenced in the callback? I think the difference with the |
no, in case dependencies are explicitly passed, references in callback should be ignored. you are telling the compiler "let me handle dependencies". |
Just to add my 2 cents. I find an extra $watch rune less confusing in this case. I'd rather have two runes that serve a similar purpose, but use two very different ways of achieving that purpose. Than to have a single rune whose behavior you can drastically change when you add a second parameter to it. |
I want to reiterate that it's really easy to create this yourself: #9248 (comment) |
On the other hand, if every project starts to define similar helpers, possibly with different argument orders and names, then this will harm the readability of code for anyone not familiar with the helpers used in that specific project and will add mental overhead to switching between projects. I feel that having an effect with a fixed set of dependencies tracked is common enough that it really should be part of the core library. |
I wholeheartedly agree with @FeldrinH, it's not just whether it's easy or difficult to achieve such outcome, but providing an idiomatic and standard way to perform something that is common enough that it's worth having it included in the official API, instead of expecting everyone to develop it's own very-similar-but-not-quite-identical solution. Anyway, providing in the docs the example provided by @dummdidumm would really encourage every one to adopt the same solution. |
Actually I would do it like this in Svelte 4: <script>
let num = 0;
let timesChanged = 0;
$: incrementTimesChanged(num);
function incrementTimesChanged() {
timesChanged++
}
</script> Which you can do the same way in Svelte 5 (EDIT: you can't, see comment below): <script>
let num = $state(0);
let timesChanged = $state(0);
$effect(() => incrementTimesChanged(num));
function incrementTimesChanged() {
timesChanged++
}
</script> But I agree that a <script>
let num = $state(0);
let timesChanged = $state(0);
$watch([num], incrementTimesChanged);
function incrementTimesChanged() {
timesChanged++
}
</script> I'm actually a fan of the Imagine a junior developer wrote 50 lines instead a |
Actually, the point is you can't do it the same way. Try it—that code will cause an infinite loop as it detects |
Wow, you're right. I really need to change my vision of reactivity with Svelte 5. This also means "Side-effects" are bad design in programming, and The previous example is actually a very good minimal example of a reactivity side-effect: <script>
let num = $state(0);
let otherNum = $state(0);
$effect(() => logWithSideEffect(num));
function logWithSideEffect(value) {
console.log(value)
console.log(otherNum) // side-effect, which triggers unwanted "side-effect reactivity"
}
</script> When reading the line with There is another caveat with the <script>
let num = $state(0);
$effect(() => {
if (shouldLog()) {
console.log({ num })
}
});
function shouldLog() {
return Math.random() > 0.3 // this is just an example
}
</script> You would expect the But that's not what will happen. It will log at first, until it will randomly (once chance out of three) stop logging forever. A <script>
let num = $state(0);
$watch(num, () => {
if (shouldLog()) {
console.log({ num })
}
});
function shouldLog() {
return Math.random() > 0.3
}
</script> |
@Gin-Quin well I agree with almost everything you're saying. Especially the lack of transparency of when an
However, this is simply a bug that I believe could be fixed. I believe some other frameworks / libraries have fixed this already. Jotai for example, say this in their docs about their
But I do fully support adding the feature to run an effect based on an explicitly defined list of dependencies. Since an |
Oh wow, this is actually very weird behaviour: let count = $state(0)
// Whole $effect callback wont run anymore
$effect(() => {
console.log('Effect runs')
if (false) {
console.log(count)
}
}) |
This is neither a bug, nor how
|
I understand, but it is not intuitive imo. let num = $state(0)
$effect(() => {
console.log('Hello')
if (false) {
console.log(num)
}
}) I would assume that the first |
@Rich-Harris that's a quite subtle but I think absolutely necessary distinction. I have reached a rather similar conclusion in a couple situations that were starting to get out of control, but couldn't quite come out with a clear and decisive rule of thumb. I hope you can find some time to explain this a bit better, with some real life example if possible, either in a blog post or, even better, some where in the docs |
@dummdidumm I tried to modify your example to
but it doesn't work. Can you explain? Why does the deps part has to be a function? |
Because for stateful variables (defined with $state/$derived/$props), passing them somewhere causes unwrapping them into either a primitive or a proxied object. So |
Thanks for explanation. Is this important behavior of rune documented (or will be documented) somewhere? I often encounter unobvious logic and flow after I upgrade my app from Svelte 4 to 5 runes. I really hope the team can expand the Rune introduction page for more details. Besides, I hope the
since function dependency is guaranteed not tracked. In Svelte 5 rune,
There have been multiple times where missing the Sure, you guys say |
I've given it more thoughts, and I think I agree with @Rich-Harris. About side effects, yes, indeed, Svelte as well as other frameworks rely on side effects. Reactivity is, per definition, a side effect: you trigger an action when a value changes. There is the risk though to get into a "reactivity hell", where your
I still think there is a sweet spot for Maybe the best thing to do is to give time to this idea and see if real-world issues come out that would be easily solved by |
I want to add that instead of seeing Both of them are only used when you don't want to track dependencies. With To answer the question about "should it be deeply reactive", a proposal would be to define |
sounds good, and perhaps instead of |
For what it's worth, SolidJS 2.0 will likely separate dependency tracking from effect execution to support their work on async reactivity. |
See also #12908 |
Perhaps, since there's an let a = $state(0);
let b = $state(0);
$effect(()=>{
track(b); //or track(()=>b) for consistency with untrack
console.log(a);
}) It would only be for readability, and would do pretty much nothing under the hood. |
Just found this issue because I was looking for a good solution to exactly the case of marking something as unsaved/dirty that was already mentioned. In my case, I have an object with a deep structure. It's holding the entire state that the user is working with so it can be saved to a file through (using spreads like the other solution isn't straightforward. There are lots of sub-properties, with some of them being arrays that are passed to multiple components in The fact that I can get fine-grained reactivity with
Based on what @Gin-Quin said, I think instead of a I don't know the specifics of how For reference, this is my current code:
|
Inspect is deleted in production code so please don't use it for such things. |
this is very comfortable to use, if only it was rune like $effect.by() or $effect.on(). it makes reading effects very easy since seeing directly what its reacting to rather reading whole function body (especially when its complex). Also avoids infinite loops. |
Yes, we should make it safer to use because it’s a powerful tool. Saying we shouldn’t make it safer is like saying we shouldn’t use any tool that could hurt us—like avoiding going outside because we might get hit by a bus! |
|
derived values are not mutable which limits usage very much. This makes me to go back to effect but I think it should be better practice to control what its reacting to if body is somewhat complex. |
Based on the (slightly tweaked) example in the very first response by @dummdidumm, I exported the explicitEffect(
() => [dep1, dep2, ...],
() => {
// do stuff
},
); Does it have any potential performance issues? And should I be worried that it will stop working after some Svelte update? |
For me, it works best just to implement a small watch function: const watch = (...deps: unknown[]): void => {
deps.forEach(dep => {
if (typeof dep === "object" && dep !== null) {
// Deep access for objects/arrays
JSON.stringify(dep) // Lightweight way to touch all properties
} else {
dep?.toString()
}
})
} And then just call it with all dependencies in the effect rune: $effect(() => {
watch(obj.arr, foo, baz)
sideEffectFn()
}); |
@caboe in this example, wouldn't |
Damn i wasn't even aware of that. Just tested it with an example. Idk...im dont really like the fact that effects do this deep tracking. This means i would have to recursively follow all the function calls in an effect to make sure it doesn't trigger without intension. |
@Blackwidow-sudo that sounds like you are always still thinking in terms of procedural programming. All modern frontend frameworks work best with the mental model of declarative programming. And in fact, once you're able to think in that way, your code suddenly becomes way cleaner than it could ever be in the procedural style. So when you think of an Of course there can be exceptions to this rule, but this rule should work for like 95% of use-cases. |
@Evertt Youre right, i was not really thinking in a declarative way. But still, i think this could be made clearer in the docs. |
Yes, it would. This could lead to infinitive loops. But this could also happen under other condition like two effects. |
Describe the problem
In Svelte 4, you can use this pattern to trigger side-effects:
However, this becomes unwieldy with runes:
Describe the proposed solution
Allow an opt-in manual tracking of dependencies:
(note: this could be a different rune, like
$effect.explicit
)Alternatives considered
but this compiles to
Importance
would make my life easier
The text was updated successfully, but these errors were encountered: