Search code examples
sveltesvelte-5

Convert Svelte 4 stores to Svelte 5 $state with Client-side Component API


Given this Svelte 4 setup REPL

<script>
    import Comp from './Comp.svelte'
    import {writable, derived, get} from 'svelte/store'

    function newCustomStore() {
        const store = writable({1: 'foo'})

        return {
            subscribe: store.subscribe,
            set: store.set,

            getEntry(id) {
                return get(store)[id]
            }
        }
    }

    function newDeviceStore(deviceMap, id) {
        return derived(deviceMap, $deviceMap => {
            return deviceMap.getEntry(id)
        })
    }

    const store = newCustomStore()
    const entryStore = newDeviceStore(store, 1)

    function handleOpenComp() {
        const comp = new Comp({
            target: document.body,
            props:{
                entry: entryStore
            }
        })
    }
</script>

<!-- not reactive  -->
{store.getEntry(1)} <br>

<!-- reactive -->
{$entryStore} <br>

<button on:click={handleOpenComp}>
    open comp
</button>

<button on:click={() => $store[1]= $store[1] + 'o'}>
    click
</button> <br>
Component.svelte
<script>
  export let entry
</script>

<!-- reactive -->
{$entry}

It works to pass a derived store to the component that is reactive to changes of the store it's derived from.
When I transform this to Svelte 5 REPL

<script>
    import { mount } from 'svelte';
    import Comp from './Comp.svelte'
    import {writable} from 'svelte/store'

    let target

    class Store {
        value = $state({1: 'foo'})

        getEntry(id) {
            return this.value[id]
        }
    }

    let store = new Store()

    let props = $derived({ entry: store.getEntry(1)})
    
    function handleOpenComp() {
        const comp = mount(Comp, {
            target: document.body,
            props: props
        })
    }
</script>

<!-- reactive -->
{store.getEntry(1)} <br>

<!-- reactive -->
{props.entry} <br>

<button onclick={handleOpenComp}>
    open comp
</button>

<button onclick={() => store.value[1] = store.value[1] + 'o'}>
    click
</button>

<br>
Comp.svelte
<script>
    let {entry} = $props()  
</script>

<!-- not reactive -->
{entry}

the $derived props won't react to changes. The docs say

"...use $state instead to create a reactive property object and manipulate it."

 const props = $state({ foo: 'bar' });
 const app = mount(App, { target: document.getElementById("app"), props });
 props.foo = 'baz';

I seem to be wrong to assume, that when passing $state should work, passing $derived should do so as well...? How can I achieve the same behaviour in Svelte 5?


Solution

  • $derived doesn't reactifies the value, the variable itself is reactive.

    For the value, you create an object and populate it with a primitive value ("foo"), so there is nothing reactive.

    The variable is reactive only within its scope. Thus, when you pass props, the value that is non-reactive from the beginning goes beyond the props scope.

    You can use closure to keep reactivity:

                props: {
                    get entry() { return props.entry }
                }
    

    Reading entry in the child component will cause reading props, and thus reactivity will be passed.