Is this the right way to create a Svelte 5 store with two-way binding to a Firebase Realtime db path?

This is how I used to do it in Svelte 4:

// $lib/firebase.ts
export function writableRealtimeStore<T>() {
  let unsubscribe: () => void = () => {}
  let objectRef: any

  const store = writable<T | null>(null)
  let storeSet = store.set

  return {
    subscribe: store.subscribe,
    set: (value: any) => {
      return set(objectRef, value)
    },
    update: () => {},
    setPath: (path: string) => {
      objectRef = ref(realtimeDB, path)
      unsubscribe()
      unsubscribe = onValue(objectRef, (snapshot) => {
        storeSet((snapshot.val() as T) ?? null)
      })
    },
  }
}

// $lib/stores.ts
export const myStore = writableRealtimeStore()

// routes/+page.svelte
<script lang="ts">
    import { myStore } from '$lib/stores'
    myStore.setPath('/books/<book_id>')
</script>

<input type="text" bind:value={myStore.bookName}

This store is reactive both ways – when the value in the DB changes, it updates the UI, and when the user updates the value of the input, the DB changes. I could access the properties of my DB object directly as myStore.bookName.

However with Svelte 5 I can’t get the same behavior of the store object:

// $lib/firebase.ts
export function createRealtimeStore<T>() {
    let unsubscribe = () => {}
    let store: { value: T | undefined } = $state({ value: undefined })
    let _ref: DatabaseReference

    return {
        get value(): T | undefined {
            return store.value
        },
        update: () => {
            if (_ref) set(_ref, store.value)
        },
        setPath: (path: string) => {
            _ref = ref(realtime, path)
            unsubscribe()
            unsubscribe = onValue(_ref, (snapshot) => {
                store.value = snapshot.val()
            })
        },
        unsubscribe,
    }
}

// $lib/stores.ts
export let myStore = createRealtimeStore()

// routes/+page.svelte
<script lang="ts">
    import { myStore } from '$lib/stores'

    myStore.setPath('/books/<book_id>')

    $effect(() => {
        if (project.value) {
            project.update()
        }
    })
</script>

<input type="text" bind:value={myStore.value.bookName}

Two problems:

  1. I must access the store’s props like myStore.value.bookName, instead of the cleaner myStore.bookName.
  2. The $effect rune must be in the page and not in the function that creates the store, because $effect can only be called during component initialization, otherwise you get an error.

Overall the Svelte 4 way of doing it was much cleaner and nice to work with and I refuse to believe that you can’t do the same thing with the new and supposedly improved store system.