sveltesvelte-5

Reactivity and exported state-using function


App.svelte:

<script>
    import Comp, {getValue} from "./Comp.svelte";

    const derivedValue = $derived.by(getValue);
</script>

<Comp />

<p>
    <span>App: clicked {getValue()} times</span>
    <span style="color: gray">{Math.random()}</span>
    <span>(derivedValue={derivedValue}).</span>
</p>

Comp.svelte:

<script module>
    let value = $state(0);

    export function getValue() {
        return value;
    }
</script>

<button onclick={() => ++value}>Clicked {value} times</button>

https://svelte.dev/playground/811fad4e3ead40ab9f20bfce06612ced?version=5.28.2

First render:
enter image description here

Clicked the button:
enter image description here

From reading Passing state into functions:

Since the compiler only operates on one file at a time, if another file imports count Svelte doesn’t know that it needs to wrap each reference in $.get and $.set

I extrapolated that the compiler, when looking at App.svelte, wouldn't know that getValue references Comp's internal state. And yet we see that:

  1. derivedValue reacted to the change: was recalculated and its span was rerendered.
  2. The span directly calling getValue was rerendered.
  3. The random value stayed the same, proving that only the mentioned spans were rerendered, not the whole parent p or another ancestor.

Both observations 1 and 2 are surprising. My questions are:

  1. How does the compiler know to react to getValue()'s change and when? Where does this fit in regards to the quoted documentation?
  2. The second observation points that getValue() itself is treated as if it was a $derived. I mean, the function is declared like a normal function, and yet if I declare a normal variable in App.svelte like let value = getValue();, value wouldn't be updated, because it's not $derived. So it seems like functions get special treatment. So should one think of all state-using functions as implicit reactive derivatives? Is there way to opt out of this (not that I need it now, but what if)? Does untrack apply?
  3. Not in the demo, but I also tried passing a state-using function from a Child component to parent via context, and the function is not a trivial getter, but has arguments and some calculations. That seems to work the same - the function is reactive in the Parent component context. So the implications of 1 and 2 apply to all kinds of functions, not just getters or something, and are not specific to module exports, but to all statically compiler-detectable ways of sharing?

Solution

  • The compiler does not need to know anything about getValue because the reactivity is communicated at runtime. The internals of the function trigger a signal which Svelte then reacts to. This applies to functions and properties of all kinds and it does not matter how deeply nested the state interaction logic is.

    The documentation is about exporting primitive state, which is not possible in a reactive way. Hence it needs to be wrapped in a function access, which you did.

    Functions do not behave the same way as $derived. They will be reactive if they internally reference some kind of state, but unlike $derived, the result is not cached and effects can re-run, even if the result of the function is unchanged. If the computation is not expensive one can just opt for functions if that is more convenient (e.g. on computed object properties).