sveltejs / svelte

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

Svelte 5: Reference to derived function does not trigger effect #13352

Closed dnass closed 1 month ago

dnass commented 1 month ago

Describe the bug

In Svelte 4, it's possible to do:

<script>
  let n = 0;
  $: func = () => n;
  $: func, console.log('func changed'); // Effect runs when n changes
</script>

<button on:click={() => n++}>Click</button>

The runes equivalent does not work:

<script>
  let n = $state(0);
  let func = $derived(() => n);
  $effect(() => {
    func, console.log('func changed'); // Effect does not run when n changes
  });
</script>

<button onclick={() => n++}>Click</button>

If func is called within the $effect callback, the effect runs. When it is only referenced, the effect does not run.

Reproduction

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE32PwWrDMAyGX0WIQh0a2p3TJFD2GMsOnS0XU1cKsRIoIe8-vHTsMNqDDvr060Oa0YdICauPGfl8I6zw1PdYot773KSJohKWmGQcbCZ1skPote2400gKDA1skp6VzFtx_KV-ZJsHjoYwkTOmgKYFXgO5NuQ9WX0M5h-seasEK5wk0j7KxWwz0iAMLAqBJ7mS266apej4pcoUz2T_RfXh7y-uv0ZVYRC2MdhrMz_O3-2W9j2T-rAmWizxJi74QA4rHUZaPpdv_HowmFQBAAA=

Logs

No response

System Info

System:
    OS: macOS 14.7
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    Memory: 361.81 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.13.1 - ~/.nvm/versions/node/v20.13.1/bin/node
    Yarn: 1.22.22 - ~/.nvm/versions/node/v20.13.1/bin/yarn
    npm: 10.5.2 - ~/.nvm/versions/node/v20.13.1/bin/npm
    pnpm: 9.11.0 - ~/.nvm/versions/node/v20.13.1/bin/pnpm
  Browsers:
    Brave Browser: 100.1.37.111
    Chrome: 128.0.6613.139
    Chrome Canary: 131.0.6728.0
    Safari: 18.0
  npmPackages:
    svelte: ^5.0.0-next.251 => 5.0.0-next.251

Severity

blocking an upgrade

Conduitry commented 1 month ago

I think one question here is whether () => n ought to be considered to change when n changes (and thus, whether func should be considered to change), and I don't know the answer to that. I'm thinking no, because evaluating () => n doesn't involve evaluating n, and so the dependence isn't established.

brunnerh commented 1 month ago

In Svelte 5 you generally should not need this pattern of capturing variables in functions via $: declarations.

Dependencies are automatically tracked across function boundaries, so the function itself not changing should not matter for practical applications.

E.g. this does not work in Svelte 4 but just does in Svelte 5:

<script>
    let n = 1;
    function double() {
        return n * 2;
    }
</script>

<button on:click={() => n++}>{n}</button>

Double: {double()}

If you have a realistic use case where it does matter, please explain it.

paoloricciuti commented 1 month ago

And if you really really need something like this you can use $derived.by

<script>
  let n = $state(0);
  let func = $derived.by(() => {
      n;
      return () => n;
   });
  $effect(() => {
    func, console.log('func changed'); // Effect does not run when n changes
  });
</script>

<button onclick={() => n++}>Click</button>
Rich-Harris commented 1 month ago

As often, the question here is 'what are you actually trying to do?'

dnass commented 1 month ago

Maybe it's an outlier, but my use case does rely on triggering an effect when a function's dependencies change. In my svelte-canvas library, users define reactive functions that encapsulate pieces of canvas rendering logic. When any render function's dependencies change, the library clears the canvas and then reruns each render function. Here's a simple example in Svelte 4.

The render functions could just rerun with every rAF tick, but that would cause wasteful renders where nothing has changed. @paoloricciuti's suggestion works, but it's extra boilerplate for users. Either solution feels like a compromise compared with what's possible in Svelte 4.

paoloricciuti commented 1 month ago

Maybe it's an outlier, but my use case does rely on triggering an effect when a function's dependencies change. In my svelte-canvas library, users define reactive functions that encapsulate pieces of canvas rendering logic. When any render function's dependencies change, the library clears the canvas and then reruns each render function. Here's a simple example in Svelte 4.

The render functions could just rerun with every rAF tick, but that would cause wasteful renders where nothing has changed. @paoloricciuti's suggestion works, but it's extra boilerplate for users. Either solution feels like a compromise compared with what's possible in Svelte 4.

I think you can change your library to just call the render function in an effect...by doing so every reactive variable inside the function will be registered as a dependency

dnass commented 1 month ago

The render functions run in a specific order. If the changed function is called in an effect, that will happen first, and then trigger every other function to re-render in order (including the changed function, at its actual position in the sequence). The upshot is that each changed render function would run twice per frame instead of once.

paoloricciuti commented 1 month ago

The render functions run in a specific order. If the changed function is called in an effect, that will happen first, and then trigger every other function to re-render in order (including the changed function, at its actual position in the sequence). The upshot is that each changed render function would run twice per frame instead of once.

Maybe I'm missing something but something like this could work I think

I've used a set but you could just as well use an array if you want to insert in specific places. Basically the point is: you shouldn't use the effect to trigger the chained render but just call every render in an effect... everytime something changes the whole chain will be rerun