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:
myStore.value.bookName
, instead of the cleaner myStore.bookName
.$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.
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.
You can create a standalone effect scope in the function creating the store via $effect.root
.