sveltejs / svelte

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

Svelte 5: Make untrack a rune #9451

Closed eddiemcconkie closed 7 months ago

eddiemcconkie commented 11 months ago

Describe the problem

The untrack function feels a little clunky to use since you have to pass it a function instead of a value. I think it could be easier to use if you could just give it an expression.

Describe the proposed solution

Would it make sense to swap the untrack function for an $untrack rune? The preview docs for $derived says:

The expression inside $derived(...) should be free of side-effects. Svelte will disallow state changes (e.g. count++) inside derived expressions.

and I think it makes sense to enforce the same behavior when untracking reactive variables to avoid side-effects. I feel like $untrack(count) is a bit easier to read than untrack(() => count). It would also be nice that you wouldn't have to import { untrack } from 'svelte'.

Alternatives considered

N/A

Importance

nice to have

brunnerh commented 11 months ago

I suspect that untrack mainly exists for $effect, so an entire block of logic, regardless of how many variable references it contains, can be excluded.

$effect(() => {
  // <reactive>
  untrack(() => {
    // <unreactive>
  });
});
eddiemcconkie commented 11 months ago

Yeah that makes sense. I guess I didn't consider that untrack can be used to do more than just return a single untracked value

dummdidumm commented 11 months ago

I'm going to reopen this to see how others feel about this. We're not entirely sure yet ourselves if we want to make it a rune or not.

benmccann commented 11 months ago

Without expressing an opinion on this particular issue, I will say that our current philosophy of only making things runes if they absolutely have to be perhaps can be difficult to approach from a user-perspective because it requires a very high degree of knowledge to know whether something would be a rune or not.

navorite commented 11 months ago

From the preview site docs:

These are introduced as functions rather than runes because they are used directly and the compiler does not need to touch them to make them function as it does with runes. However, these functions may still use Svelte internals.

I think that should not define whether something should be a rune or not. Runes is more of a marketing term and it should be so. It makes more sense to me to make it a rune as it purely exists for usage inside $effect and $derived runes, and it is part of what defines the Svelte 5 reactivity system.

dummdidumm commented 11 months ago

Another argument of making it a rune: By the logic of "only make things a rune that absolutely have to", $effect also wouldn't be a rune, because you could define it as a function that is a noop in SSR and so bundlers would dead-code-eleminate everything inside.

adiguba commented 11 months ago

Hello,

Just my two-cents.

IMHO the rune is more logical (and more concise) when accessing a state/prop.

$state, $derived and $props runes allow us to declare reactive states/props. It's seem logical that another runes allows us to temporarily deactivate this reactivity on a specific state/prop.

And, we should use two syntax :

adiguba commented 10 months ago

Also, not sure if I should post here or create another issue, but I think that untrack should handle proxified object/array. If the return value is a proxified object/array, it should be automatically untracked.

For example image this :

    let array = $state([ ... ]);
    let value = $state('val');

    $effect(() => {
        const a = untrack(() => array);
        console.log("array length: " + a.length);
        console.log("includes value: " + a.includes(value));
    });

Here array is still tracked because a.length and a.includes(value) are proxified. To fix that, we must wrap all uses of array with untrack(), which is less readable :

    $effect(() => {
        console.log("array length: " + untrack(() => array.length));
        console.log("includes value: " + untrack(() => array.includes(value)));
        value; // needed to track value, because of untrack
    });

I think that this line untrack(() => array) should handle this case, by returning a proxified array/object that use untrack internally.

aradalvand commented 10 months ago

A variation of $effect with explicit dependencies could cover most of the use cases that untrack is used for — see #9248.

Instead of making untrack a rune, if you're going to add a rune, why not just add an explicit effect (a.k.a. $watch) instead?

adiguba commented 10 months ago

With automatic dependencies, I don't see any interest of explicit dependencies. It's a regression.

I don't want explicit dependencies, just a simplier way to untrack some states/props...

steve-taylor commented 9 months ago

our current philosophy of only making things runes if they absolutely have to be perhaps can be difficult to approach from a user-perspective because it requires a very high degree of knowledge to know whether something would be a rune or not.

I hope the philosophy has since shifted further away from this towards creating a more consistent developer experience. $untrack(value) seems more Svelte-like to me than untrack(() => value).

opensas commented 8 months ago

A variation of $effect with explicit dependencies could cover most of the use cases that untrack is used for — see #9248.

Instead of making untrack a rune, if you're going to add a rune, why not just add an explicit effect (a.k.a. $watch) instead?

It happened to me a couple times that I needed to manually control which variables triggered reactivity, and in all those cases it was easier for me to specify which variables to track, and not which ones to untrack. I just created a function receiving only the variables I want to track, even though inside the function I might access other top-level (and thus reactive) variables.

It seems to me like that would be the most common scenario. So I think something like https://github.com/sveltejs/svelte/issues/9248 would be very useful indeed.

leoj3n commented 7 months ago

One major benefit of a rune is auto imports for that feature.

For me the problem with untrack as a function and not a rune is that you have to import a first-class library feature, where as almost every other userland feature you would use in a component for example is a rune without any import.

In this way import can be reserved for exposing rare internal library or third party features, and so instead of drawing the line at "does the compiler touch this" it can be rather "is this a first-class framework feature that is inconsistent and annoying to have to import".

eddiemcconkie commented 7 months ago

Tbf onMount, on destroy, before/after/onNavigate are all first-class framework features that have to be imported

leoj3n commented 7 months ago

@eddiemcconkie onMount is deprecated in favor of $effect in 5, but I'm not sure about onNavigate.

As a user I would want to have $navigate act like yet another side-effectual "rune" like $effect.

The point is it interrupts the flow in userland to have to think about which functionality needs to be imported, and that it is really only beneficial to the library author to know the distinction between compiler-only functions.

leoj3n commented 7 months ago

Something I am unsure of, is if there are people using svelte without the compilation step. That is the only reason i can think to maintain the ability to import the non-compiler functions.

Otherwise since you've already started down this path with $:, $count, on:click, and now runes, might as well go all the way. If someone is opinionated against the compiler magic and wants to see everything that is used imported at the top of their script tag they probably shouldn't be using svelte to begin with, right?

Does anyone remember classes autoload in PHP? I actually find it much nicer when you have a subset of known functionality that you want to tap into to not have to junk up files in the same project space with includes...

https://www.php.net/manual/en/language.oop5.autoload.php

leoj3n commented 7 months ago

If we slightly modify that definition:

https://svelte-5-preview.vercel.app/docs/runes

Runes are function-like symbols that provide instructions to the Svelte compiler. You don't need to import them from anywhere — when you use Svelte, they're part of the language.

Then for a rune you can ask:

eddiemcconkie commented 7 months ago

Looking at the preview docs, before/afterUpdate are getting deprecated. I believe onMount is sticking around. You can use an effect to do the same thing as onMount, but you'd need to untrack any reactive state manually if you just want to run something once when the component mounts, so I think onMount is still more convenient in some cases. onNavigate is useful for view transitions.

The point is it interrupts the flow in userland to have to think about which functionality needs to be imported

Honestly this isn't much of a concern when your text editor does auto imports. Svelte tries to rely on web standards and native language features as much as possible and sprinkle in a little magic when needed, so it makes sense to import most of Svelte's features since it's just regular JavaScript code, and only make runes globally accessible since they're not JavaScript but part of the Svelte language.

Originally I thought untrack would be better as a rune, but I do think what the Svelte team said about only making things runes when necessary makes a lot of sense and I can get on board with that.

leoj3n commented 7 months ago

I do think what the Svelte team said about only making things runes when necessary makes a lot of sense and I can get on board with that.

@eddiemcconkie I respect that, but whole-heartedly disagree. You mentioning the IDE auto-import is rather ironic considering how that's a poor man's version of what I think would be the more robust option.

eddiemcconkie commented 7 months ago

Svelte exports more than just lifecycle functions. There are also things like stores or transition functions. Explicit imports help avoid naming collisions. It probably also makes it much easier for the Svelte team since they can just write and export plain JavaScript and not have to worry about making everything globally available

leoj3n commented 7 months ago

@eddiemcconkie For the no-compiler-required functions I would imagine they would still just do the import for use behind the scenes. If this is the strategy I'm not sure there is much of a scope issue outside of having to document and stub out more "runes". As long as runes a prefixed with $ what naming conflicts are you envisioning?

I'm not too familiar with it but with tree-shaking isn't it also a non issue if they just put everything on a global $?

You specifically called out transitions, isn't that already it's own module? https://svelte.dev/docs/svelte-transition

Not like untrack which you import directly from svelte. That could perhaps disqualify transitions from rune status, and you would continue to recommend importing that when needed.

EDIT:

For reference, here is all the modules in svelte: https://github.com/sveltejs/svelte/tree/main/packages/svelte/src

I'm thinking it wouldn't be too crazy to have all these be runes... I'll have to look at it closer.

leoj3n commented 7 months ago

Tbf onMount, on destroy, before/after/onNavigate are all first-class framework features that have to be imported

I've only ever used SvelteKit, not svelte alone.

Didn't think about it when you mentioned onNavigate... but that is definitely a part of SvelteKit and not plain svelte.

However if we ask "Is it function-like?" Answer: Yes. "Do you HAVE to import them from somewhere?" Answer: It's core functionality in Kit. "Is it part of the language?" Answer: It's part of the "language" of Kit. So, if we were to follow the modified definition of what we call a rune, this means Kit could have $runes like $navigate, too.

You are right about onMount, I misinterpreted those docs:

Additionally, you may prefer to use effects in some places where you previously used onMount and afterUpdate (the latter of which will be deprecated in Svelte 5).

Regarding my modified idea of what a rune is, in regards to the issue you brought up of all the non-core modules that are still "part" of svelte, I went through them and have some thoughts:

I understand that I am more or less hijacking the original intention of runes here to mean something that you would otherwise from svelte (or kit). You probably ascribe lower level meaning to rune but I fear users will not.

I hope you can see that now because 5 is being a more expressive than the previously transparently overriding let and what would otherwise be normal JS variables, etc, I am starting to see $ as a potential all-out prefix for what Svelte provides, and because of it being a compiler this should be possible to implement in the way I am describing, I think.

In fact, as a user, I want and expect this. But, as a framework author I understand you may value the original intention more than what users expect, and they will just have to learn that some very specific parts are compiled out (the current runes), but not all parts. The unfamiliar user will not know why this is the case, and may expect what I am saying here because they don't really care to know the reason why the first runes were created (when previously they were essentially writing slightly enhanced JS), but they will make do either way I am sure. Personally, I still feel this is a viable idea, because you can have a technical document for those who want to learn more about how the internals work, explaining which "runes" are special.

Ultimately I will use Svelte either way, I just foresee a lot of users being confused by the difference in usage pattern between runes and other "parts of the language" provided by svelte/kit, and not really caring why it is different, and instead just wanting one consistent pattern if they could.

My thoughts are that only contributors and framework authors are going to want to understand the difference between $effect and $mount, although I could be coming too hard from my own userland perspective. Hope what I'm saying is coming across legibly, as I do believe what I'm saying is technically possible to implement, even if not desired or expected as someone who is thinking on a deeper level about the implementation of the primitives.

Essentially the whole import difference makes me want to have $runes in general, of which a subset are more "primitive" $runes that have no other way to be implemented.

A lot of words, I but I THINK I have a point. May be wrong though.

EIDT:

Just to get back to where this all started with untrack as an example; I think boils down to usage versus implementation details...

This feels a lot more natural:

<script>
    let size = $state(50);
    let color = $state('#ff3e00');

    let canvas;

    $effect(() => {
        const context = canvas.getContext('2d');
        context.clearRect(0, 0, canvas.width, canvas.height);

        // this will re-run whenever `color` or `size` change
        context.fillStyle = color;
        $untrack(() => {
            context.fillRect(0, 0, size, size);
        })
    });
</script>

Than this:

<script>
    import { untrack } from 'svelte';

    let size = $state(50); // Just doesn't feel right to have magic here...
    let color = $state('#ff3e00');

    let canvas;

    $effect(() => {
        const context = canvas.getContext('2d');
        context.clearRect(0, 0, canvas.width, canvas.height);

        // this will re-run whenever `color` or `size` change
        context.fillStyle = color;
        untrack(() => { // But not any magic here...
            context.fillRect(0, 0, size, size);
        })
    });
</script>

I feel it is not important for users to understand why untrack is having to be imported for its functionality when state and effect are not, because svelte is compiler. As a user you just don't really care if it works and is easy. Now, if there is a very important reason why the user should understand this distinction, then maybe that trumps this idea.

leoj3n commented 7 months ago

Feel free to mark my comments as off-topic. Prolly should have started a different thread. Just thought it sounded like a plausible idea to put everything svelte-related behind $ and compile out the need for importing those things. Probably too many unknowns being different from standard JS and potential name collision between kit and underlying svelte, etc as @eddiemcconkie mentioned. Still an intriguing idea to me but probably impractical. Ppl will just have to wrap heads around the syntax that differs.

Gin-Quin commented 7 months ago

Untracking a value and untracking a block of code are two very different things. It feels quite weird that the current untrack function do both. It untracks the callback content, then the returned value, if there is one.

IMO, the api should have:

  1. one rune $untrack(value: ReactiveValue) to untrack a single value (the most common use case)
  2. and one function (or another rune) untracked(callback: () => void) to run many lines of code without triggering any reactivity
Blankeos commented 7 months ago

@leoj3n

onMount is deprecated in favor of $effect in 5, but I'm not sure about onNavigate.

I wonder if this still holds true? I know these docs are subject-to-change but. I don't see onMount to be deprecated here: https://svelte-5-preview.vercel.app/docs/deprecations

Also from this page: https://svelte-5-preview.vercel.app/docs/runes

image

I think afterUpdate (the latter), will be deprecated. It's a little unclear though if the "latter" in this statement is


Also for me, I wonder why onMount should be deprecated? The $effect rune will run every time a reactive $derived and $state inside it changes. Wouldn't that trigger unwanted executions?

If I wanted to run something strictly "on mount", (e.g. a useEffect without deps), then I would use onMount, correct? If that were to be deprecated, what would I use in that scenario?

PuruVJ commented 7 months ago

onMount won't be deprecated. It will just be shipped as const onMount = func => $effect(untrack(func()))

dummdidumm commented 7 months ago

We decided against making untrack a rune, given how rare its usage is.