sveltejs / svelte

web development for the rest of us
https://svelte.dev
MIT License
80.25k stars 4.27k forks source link

User experience with $effect #14247

Open dm-de opened 2 weeks ago

dm-de commented 2 weeks ago

Describe the problem

While beta testing and after porting a Svelte 4 project, would like to share these experiences and recommendations for improvement. I made a similar post before and it was closed very quickly. This post is slightly different. I hope it forms a discussion.

✔️=good / ❌=bad


I never had any problems with $: The changes were tracked automatically. There were no infinite loops. It was predictable - with a few extremely rare exceptions that I never encountered. The frustration level was extremely low.

To summarize for $:

✔️ automatic detection of updates, no manual handling of dependencies ✔️ variable changes did not cause infinite loops - and there were no errors that stopped execution ✔️ no deep scan of dependencies ❌ in very rare cases not expected behavior


Reacts useEffect.

I think useEffect frustrates people because you have to manually create a dependency array. And if you forget something, it doesn't work. Sometimes it's difficult to work with. But there are few rules and it's predictable.

To summarize for useEffect: ✔️ Few rules and predictable ❌ manual dependency arrays ❌ endless loops possible


$effect.

Basic rule: When automatism comes into play - it sometimes becomes unpredictable. While $: managed to find a good balance, this is not yet the case with $effect. It is necessary to learn a lot of rules - and the behavior is often still unexpected. Even though $effect works exactly the opposite of useEffect - the level of frustration is comparable. $effect has even more disadvantages.

To summarize for $effect:

✔️ automatic tracking of dependencies ❌ Endless loops possible - and even stopping the execution ❌ deep scan of dependencies in nested functions (which I see as a disadvantage and it should be optional) - (improvement 1) ❌ non-transparent: you cannot see which dependencies have been created - e.g. with useEffect you can see the dependency array. This was not necessary with Svelte 4 because it was simpler. (Improvement 2) ❌ No good way to completely disable tracking for certain reads (improvement 3) ❌ Very hard to debug. It is not displayed which effect (file and position) triggers an infinite loop and you have to search for it manually. For example, 10 last elements shown - but I couldn't see where it happened because it was even deeper. (Improvement 4) ❌ many rules to learn - I encounted many things, that I don't expected. And still today - I'm not sure to know all gotchas.

Describe the proposed solution

Recommendations for improvements:

(1) Make depth scan optional. e.g. new rune or option: $effect.deep - for depth scan or vice versa $effect.near - for no depth scan or option $effect(fn, deep)

(2) Output of the dependency array created $effect returns an array of the dependency arrays how to display it? $inspect($effect) I am not sure

(3) Disable tracking completely $effect(fn, [var1, var2, notrack...]) or

$effect(() => {
$notrack(var1, var2, notrack...)
...code...
})

(4) Better debugging: Output file and position in the event of an error

Importance

would make my life easier

brunnerh commented 2 weeks ago

All of the tracking reductions can be done in user-land by wrapping $effect and using untrack.

To prevent deep tracking, wrap any calls in untrack. To have explicit dependencies, use a function that separates dependencies and effects and untracks the effect function (see https://github.com/sveltejs/svelte/issues/9248#issuecomment-1732246774).

many rules to learn

I do not see that being the case. You are probably just not used to it yet and fail to acknowledge all the rules that came with $: (and the respective reactivity system), e.g.

MotionlessTrain commented 2 weeks ago

I’d say that the deep tracking of $effect makes it much more predictable than the old reactive statement syntax was. It is not intuitive for starting svelte 3/4 developers that having a variable used in a function which is called in a $: does not make it reactive, and you need to give that variable explicitly as argument to the function, or abuse the comma operator to have it work. It is also a strange work around to have something not being reactive. The fact that everything is tracked with $effect makes that much more consistent, and untrack() makes it much clearer that you are using it to not track variables, instead of making a seemingly redundant function

brunnerh commented 2 weeks ago

Yes, GitHub, that is definitely math... CC. @MotionlessTrain

dm-de commented 2 weeks ago

To prevent deep tracking, wrap any calls in untrack.

I try to reproduce this:

$: run(a, b)

a & b are watched. but in run function - here is no more deep tracking

Below code will not do the same, because a & are now untracked.

<script>
    import {untrack} from 'svelte'
    let a = $state(0)
    let b = $state(0)

    $effect(() => {
        untrack(() => run(a, b))
    })

    function run(a_, b_) {
        //...
    }
</script>

My next idea was to untrack later...

<script>
    import {untrack} from 'svelte'
    let a = $state(0)
    let b = $state(0)

    $effect(() => run(a, b))

    function run(a_, b_) {
        untrack(() => run2(a_, b_))
    }

    function run2(a_, b_) {
    //...   
    }
</script>

A shorter version could be this:

<script>
    import {untrack} from 'svelte'
    let a = $state(0)
    let b = $state(0)

    $effect(() => {
        let tmp_a = a
        let tmp_b = b
        untrack(() => run(tmp_a, tmp_b))
    })

    function run(a_, b_) {
    //...   
    }
</script>

Can you see, how complicated this is compared to $: ?

To my surprise... A short example works... But in my real project - this do not work as expected... I need to understand why...

I didn't immediately come up with such an idea. Only, after seeing next example in your link. It would be good to have such a tip in the docu. And further improvements.


To have explicit dependencies, use a function that separates dependencies and effects and untracks the effect function (see https://github.com/sveltejs/svelte/issues/9248#issuecomment-1732246774).

very fascinating and short - missing this in the docs!

brunnerh commented 2 weeks ago

You don't need to store & use temporary variables, e.g. both of these work;

$effect(() => {
    a, b;
    untrack(() => run(a, b))
})

Playground

$effect(() => {
    run(a, b)
})

function run(a, b) {
    untrack(() => c = c + 1);
}

Playground

Though if the function does not use the args, there is no point in passing the values. This is also one of those commonly seen hacks when $: is used: Pointlessly passing around variables that are already in scope to trigger when those variables change.

dm-de commented 2 weeks ago

But in my real project - this do not work as expected...

It works now...

.

I summarize for now:

(1) Make depth scan optional. e.g. new rune or option: ✔️ is possible with untracked function and previously readed variables more verbose than $:

(2) Output of the dependency array created ❓ not sure, if this is required, because (1) and (3) are usable but could be helpful

(3) Disable tracking completely ✔️ is possible with wrapped effect - like useEffect

(4) Better debugging ❌ error reporting in case of endless loop is not good enough

webJose commented 2 weeks ago

This mere spectator's opinion is that it would be amazing if one could obtain the list of signals an effect depends on. It would make debugging next level easier.