componentssveltereactive-programmingsvelte-5

Svelte 5: How to pass primitives reactivly across components/modules


I have a few dozens of reactive primitive values (like e.g. div_status = "frozen") which I need to pass between component/module boundaries, including +page.svelte files and svelte.js modules. These values represent changable properties of the UI, and will hence influence (a) styles as well as (b) if-else blocks inside some event listeners for user interaction. Importantly, the values need to be reactively on a drag-level, since the UI must respond while the user is dragging (e.g. hovering over a dropzone, dragging for a specific distance, etc.)

Transforming my code base to Svelte 5, I'm not sure, whether to use stores or $state() for this use case.

Lastly, I am confused, whether I should look into how getters/setters work and whether the choice of either stores or $state() depends on my choice of using context-API or just passing them around as props. All in all I'm looking for a solution that is easy to refactor.

Update

I ended up using $state together with getters/setters. For anybody reading this until the official Svelte 5 Docs are out: I very much recommend watching this video of Rich Harris, which explains how to do this, and why it's NOT MORE code to write, even at first glance, you may think so.

I don't use stores any more, but for anybody interested in using them I'll write my code in a separate answer to this question. It will also work in in svelte.js files.


Solution

  • Reassignment of $state is not a problem in itself. Primitives being passed by value prevents updates; this can also be observed in plain JS, e.g.

    function createCounter() {
      let value = 0;
      setTimeout(() => value++); // happens later
      
      return value;
    }
    
    const counter = createCounter();
    console.log('Value copied', counter)
    setTimeout(() => console.log('Value copied', counter), 1);

    You need to maintain a reference to the value, either via a function or an accessor property.

    function createCounter() {
      let value = 0;
      setTimeout(() => value++); // happens later
      
      return () => value;
    }
    
    const counter = createCounter();
    console.log('Value referenced', counter())
    setTimeout(() => console.log('Value referenced', counter()), 1);

    Generally one should use runes over stores in Svelte 5; stores are likely to be deprecated in later versions as runes should be able to replace them.

    If you have a read-only state, you can return a function as shown in the example above and "receive" that via $derived.by. E.g.

    function createCounter() {
        let value = $state(0);
        setTimeout(() => value++); // happens later
        
        return () => value;
    }
    
    const counter = $derived.by(createCounter());
    $inspect('Counter', counter);
    

    Preview REPL1

    For write access there are also various options, e.g. using two functions (get/set) or using a { value } box. The box approach is probably the most flexible as it allows the object to be used in things like bind:value={...}.

    <script>
        function loggedValue(initial) {
            const state = $state({ value: initial });
            $effect(() => {
                console.log(JSON.stringify(state.value));
            });
    
            return state;
        }
    
        const count = loggedValue(0);
    </script>
    
    <input type=number bind:value={count.value} />
    

    Preview REPL1

    You can also easily pass such an object into a context; contexts are primarily useful for passing something multiple levels deeper into the component hierarchy. As noted, with props you can directly use a binding on the value to send and receive updates to the value.

    Having many primitives that are being passed around might hint at optimization opportunities. Maybe some of them could be grouped and put on a larger $state object with more meaningful property names than "value".

    What to use often depends on the exact issue at hand. Sometimes only providing read access to the state and bespoke update functions might be the best approach.


    1 Links might break later and require updating.