javascriptfirebasefirebase-realtime-databasesveltesveltekit

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 (myStore.value) {
            myStore.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.


Solution

    1. You can still use myStore.bookName, though that would require spreading the data on the state object. To tell whether data has actually been loaded, you probably would want a separate flag.

      Object.assign(store, snapshot.val());
      

      Would also return the various functions separately from the state object in this case so there are no conflicts. Whether this is better than having the value wrapper is debatable.

    2. You can create a standalone effect scope in the function creating the store via $effect.root.