svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Svelte Reactivity Bug with `$derived` in class

Open timephy opened this issue 11 months ago • 7 comments

Describe the bug

I used a "getter", a$derived and a $state together inside a class.

This worked before, but after updating to the latest version it stopped being reliable.

I noticed that reactivity to the below mentioned get layout() does not work reliably. (it does start working in some conditions I could not understand)


→ TLDR: Using $derived inside a class is unreliable, using a "getter" instead is not.

This version created a reactivity problem. Where even embedding the text {new Layout().layout} would not be reactive to changes of #orientation.

class Layout {
  #orientation: OrientationType = $state("portrait-primary")
  #orientationLock: OrientationType | null = $state(null)
  #orientationEffective = $derived(this.#orientationLock ?? this.#orientation)
  #isHorizontal = $derived(this.#orientationEffective.startsWith("landscape"))

  // this private state is then exported using a getter

  get layout(): LayoutType {
        return this.#isLargeWidth
            ? "with-sidebar"
            : this.#isHorizontal && PLATFORM === "mobile"
              ? "with-sidebar-tiny"
              : "only-main"
  }
}

→ This problem was solved when changing the $derived variables into getters themselves:

// ...
    get #orientationEffective() {
        return this.#orientationLock ?? this.#orientation
    }
    get #isHorizontal() {
        return this.#orientationEffective.startsWith("landscape")
    }
// ...

Additional context: I use the resulting state inside a snippet and pass this snippet to another component to render it.

{#snippet snip()}
    <PlayerTab
        layout={LAYOUT.layout === "with-sidebar-tiny" ? "landscape" : "normal"}
    />
{/snippet}

Observing the state from the parent is NOT enough to fix the bug:

{LAYOUT.layout} 
{#snippet snip()}
    {LAYOUT.layout}
    <PlayerTab
        layout={LAYOUT.layout === "with-sidebar-tiny" ? "landscape" : "normal"}
    />
{/snippet}

HOWEVER observing it from inside <PlayerTab /> ({LAYOUT.layout}) IS ENOUGH to fix it. Observing only the layout prop (as text: {layout}) is however not enough... How weird. LAYOUT.layout and layout are both used in multiple {#if} blocks/etc inside PlayerTab.

Reproduction

I am very sorry to say that I was not able to reproduce this in the playground (tried for around 30 minutes)

→ However the described bug is 100% reliable, and also 100% fixable by changing $deriveds into "getter"s!

→ (It also seems to be related to the use of null ?? "fallback" inside $derived...?)

Logs


System Info

System:
    OS: macOS 15.4.1
    CPU: (20) x64 Intel(R) Xeon(R) W-2150B CPU @ 3.00GHz
    Memory: 205.15 MB / 64.00 GB
    Shell: 3.2.57 - /bin/bash
  Binaries:
    Node: 23.11.0 - /usr/local/bin/node
    Yarn: 1.22.11 - /usr/local/bin/yarn
    npm: 10.9.2 - /usr/local/bin/npm
    pnpm: 10.7.1 - /usr/local/bin/pnpm
    bun: 1.0.25 - /usr/local/bin/bun
  Browsers:
    Chrome: 136.0.7103.48
    Safari: 18.4
  npmPackages:
    svelte: 5.28.2 => 5.28.2

Severity

blocking an upgrade

timephy avatar Apr 30 '25 10:04 timephy

Okay, I managed to create a (similar) reproduction. https://svelte.dev/playground/65217d0486a04acf9c5f2c8275fe63b8?version=5.28.2

  • It only causes the bug when commenting in+out the observation <p>{CLS.countP2}</p> in App.svelte
  • It is NOT fixed by changing $derived to a "getter"
  • Observing layout inside Comp.svelte is NOT enough to fix it.

This behaviour differs from the bug described before. Maybe these are not even related.....

Because of this obscure behaviour, please see this screen recording. And see that after commenting the observation for the second time, the reactivity breaks.

https://github.com/user-attachments/assets/80e45639-e7ff-4881-9a8c-cafd1ce870e3

timephy avatar Apr 30 '25 11:04 timephy

Might be related to #15829 and #15832.

timephy avatar Apr 30 '25 11:04 timephy

Having the same problem with Svelte 5.28.2. The $derived in class is not reactive.

My work-around for now is to add an $inspect(classA_instance.derivedX) in the component's script, then it works.

hermit99 avatar May 06 '25 08:05 hermit99

@hermit99 $inspect is dev-only and doesn't exist in prod.

7nik avatar May 06 '25 10:05 7nik

@timephy @7nik To help you guys diagnose, I took some time to create a simplified repo: https://svelte.dev/playground/a3ff881bd3bb4011b9c3044ea22a876b?version=5.28.2

Notice the template is not updated. However if we comment out line 5 in main: ui.errors.e1 = null then everything is working again.

Can also confirm it's working fine on [email protected]: https://svelte.dev/playground/a3ff881bd3bb4011b9c3044ea22a876b?version=5.28.1

From changes between 5.28.1 and 5.28.2, I doubt commit d0dcc0b79534f377349e7f6aa4a97c7b55f51864 or bfb969a6cccb92180180416641e48889eab730a6 are most likely the cause?

hermit99 avatar May 22 '25 03:05 hermit99

Looking at the REPL, I'm quite sure it is #15829 where the cause are deriveds created outside the reactive context + re-calculating them into the save value as previous one.

7nik avatar May 22 '25 07:05 7nik

Hey,

  • This issue is breaking reactivity
  • A main feature of Svelte
  • With something as common as using a standard TypeScript feature || and ??

Could someone please pay a little attention to this and fix it?

timephy avatar Jun 12 '25 10:06 timephy

Closed by #16249

paoloricciuti avatar Jun 28 '25 08:06 paoloricciuti

I'm not totally sure yet tbh because there was no real reproduction from the OP, but we can reopen in case it didn't fix it

dummdidumm avatar Jun 28 '25 09:06 dummdidumm

Uh I did test the reproduction that I found and was fixed...but yeah I'm pretty sure it was the same

paoloricciuti avatar Jun 28 '25 09:06 paoloricciuti

I was breaking my head over why my state from class wasn't updating in my components. Upgraded from svelte 5.34.5 to 5.35.1 (latest) and it was fixed. Not 100% sure if it was the same issue as OP was having but at least something got fixed😄

Laurens256 avatar Jul 03 '25 11:07 Laurens256

I too was having strange reactivity issues with $derived in an exported class, where it would only seem to start working after a hot module reload on the dev server (and I never tested if it persisted into production builds). Was on Svelte 5.33.14, upgrading to 5.36.13 did the trick!

martinm07 avatar Jul 22 '25 16:07 martinm07