Michael Uhlenberg
Michael Uhlenberg

Reputation: 41

Why do I have to use $derived when destructuring a class with states?

I am writing a Sveltekit app with Svelte 5 after taking a course from Niklas Fischer. The pattern is as follows: Mostly all state is contained in a global State class. In routes/+svelte.layout.ts the State class is instantiated and then put in a context with setContext. In various +page.svelte files the State is conveniently obtained with state = getContext(key). Quite often the State is destructured, e.g. let { statevar1, statevar2 } = $derived(state). It must be $derived to work, just destructuring the state does not work, nor destructuring $state(state). I can understand what let doubled = $derived(count*2) means, but here we are not changing the state variables, and worse, we may not change them later in the code, as you may not assign to derived values.

I wrote a small test to find out what works and what not. In test.svelte.ts

class Counter {
    cnt = $state(1);
    constructor(start: number) {
        this.cnt = start;
    }
    inc() {
        console.log('inc before', this.cnt);
        this.cnt += 1;
        console.log('inc after', this.cnt);
    }
    init() {
        console.log('init before', this.cnt);
        this.cnt = 100;
        console.log('init after', this.cnt);
    }
}
export const counter1 = new Counter(10);
export const counter2 = $state({
    cnt: 50
});

In +page.svelte

<script lang="ts">
    import { counter1, counter2 } from './test.svelte';
    let { cnt: cnt1a } = counter1;
    let { cnt: cnt1b } = $state(counter1);
    let { cnt: cnt1c } = $derived(counter1);
    let { cnt: cnt2a } = counter2;
    let { cnt: cnt2b } = $state(counter2);
    let { cnt: cnt2c } = $derived(counter2);
</script>

<h1>Counter1</h1>
<h1>{counter1.cnt} works</h1>
<h1>{cnt1a} fails</h1>
<h1>{cnt1b} fails</h1>
<h1>{cnt1c} works</h1>
<button onclick={() => counter1.init()}>init1 works</button>
<button onclick={counter1.init}>init2 fails</button>
<button onclick={() => counter1.inc()}>inc1 works</button>
<button onclick={counter1.inc}>inc2 fails</button>

<h1>Counter2</h1>
<h1>{counter2.cnt} works</h1>
<h1>{cnt2a} fails</h1>
<h1>{cnt2b} fails</h1>
<h1>{cnt2c} works</h1>
<button onclick={() => counter2.cnt++}>inc2</button>

When this page runs, the buttons init2 and inc2 do not work, because class methods can not be passed as function arguments (what a pity, I assume this is due to JS, not Svelte). With init1 and inc1 only the counter counter1.cnt is reactive, and the derived counter cnt1c. But cnt2b is not, as I had assumed. And cnt1a is also not reactive, which indicates that destructuring strips off the reactivity. Why is all that so?

In additon: In the class I must initialize with some value, which is immediately overwritten by the constructor. And I must initialize with $state(), but I must not use $state in the constructor, i.e. I may not write this.cnt=$state(start). I have not even tried to call new Counter() with a $stated variable...

Upvotes: 0

Views: 75

Answers (1)

brunnerh
brunnerh

Reputation: 185280

Destructuring copies primitive values. If you don't use $derived, this only happens once so the variable will never update.

This is analogous to regular assignments; this will also never update:

let cnt1a = counter1.cnt;

A destructuring with $derived can also be seen as multiple separate assignments using $derived (which is also what happens internally).

This:

let x = $state({ a: 0, b: 0 });
let { a, b } = $derived(x);

Becomes:

let x = $.proxy({ a: 0, b: 0 });

let a = $.derived(() => x.a),
    b = $.derived(() => x.b);

Upvotes: 1

Related Questions