Skip to content

Svelte Reactivity Bug with $derived in class #15853

Open
@timephy

Description

@timephy

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    p0stuff we should fix ASAP

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions