-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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 |
To respond to some of those points:
That's actually the far less common use case, the more common one is where the function accesses
To me, |
@Rich-Harris thanks for explaining some of the reasons against |
Can you give an example of where you're doing that? Would love to better understand it. I think it's instructive to look at the example of React. Firstly, a lot of React users strongly dislike the dependencies array and look at the corresponding APIs in reactive frameworks with jealousy, so I think people would be surprised at this discussion. But secondly, even though there's an explicit dependency array — the thing people are asking for here — it's very much not a way to actually control the dependencies. It's just a way to tell the runtime about them. Your linter will yell at you if you omit |
@Rich-Harris Thanks for the input! I've been swayed into thinking this is a footgun after reading the responses, but just to address some points:
$watch(() => {
// ...
}, dep1, dep2); |
As far as I can tell this is a perfect example of an effect that should just be an event handler 😀 <script>
- import {untrack} from 'svelte';
let dialogOpen = $state(false);
let initialText = $state('');
let input = $state('');
- $effect(() => {
- untrack(() => console.log('effect triggered', dialogOpen, input, initialText));
- dialogOpen;
- untrack(() => {
- input = initialText;
- });
- });
</script>
<label>
Initial Text: <input bind:value={initialText} />
</label>
<br/>
-<button onclick={() => (dialogOpen = true)} disabled={dialogOpen}>Open dialog</button>
+<button onclick={() => (dialogOpen = true, input = initialText)} disabled={dialogOpen}>Open dialog</button>
<br/><br/><br/>
It's basically equivalent, because in normal JS you could still do |
I disagree - often you can close a dialog in multiple ways (button press, click outside, pressing escape, native |
That sounds like an API or approach issue. Regardless of reason, when a dialog closes, it should fire a Personally, I would not reset on close anyway; instead either on open or not at all by just not reusing dialog instances (create/destroy instances via client-side API - snippets should make this even more convenient). |
Example without any $effect. Talking about the dialog example, it is weird to reuse it. A normal code looks like this: {#if showDialog}
<MyDialog prop="value" anotherProp={variable} onclosed={onDialogClosed}>
some custom markup
</MyDialog>
{/if} But I prefer to wrap dialogs into a function so you can do // data preparation
let result = await showDialog(params);
if (result) {
// do this
} else {
// do that
} In both cases, you don't have the data resetting problem.
My claim is based on observation - I have barely seen people adjusting the dependencies list and attempts to take full control over it only here. I think the dev team will say they observe nearly the same statistics. And, as was said multiple times, it's super easy to make your own $watch with the desired design and shallow/deep/configurable watching. |
As noted above, a But suppose that's not the case, and you really do have a situation where The central thing to understand is this: effects are about doing things in response to state being a certain way, not in response to state changing in a certain way. Take canvas rendering for example — I'm drawing a red circle because When you use the effect, you're introducing an insidious form of indirection, which is responsible for most incidental complexity in programming. The more indirection you can eliminate, the happier you'll be maintaining the code. A good rule of thumb is that if you're setting state inside an effect, you're probably doing it wrong. There are enough exceptions to stop us from actually enforcing that (i.e. throwing an error if you set state in an effect) but it's a good rule nonetheless. |
Great point! This clarifies things for me. I think in my case, the point of a
@7nik , I don't think your todo example scales, no? It would require a context or additional prop drilling to be used in child components, and you would have to remember to mutate One other thing I stumbled across while looking through my old code is a few instances where I would want to set two variables based on one Anyway, I would be fine implementing these cases manually with untrack, given how rare they are. I'm happy to close this issue. |
Why todo list won't scale? All inputs emit the |
Which is easier to scale, having one source of truth at the root of your form or making sure that every single way to mutate it emits a custom |
Two places are so many. |
@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. |
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: