skip to Main Content

I have a factory function that creates a svelte store, which is bound to a Firebase Realtime object:

// $lib/firebase.ts
export function realtimeStore<Project>(path: string) {
  let unsubscribe: () => void

  const projectRef = ref(realtimeDB, path)

  const { subscribe } = writable<Project | null>(null, (set) => {
    unsubscribe = onValue(projectRef, (snapshot) => {
      set((snapshot.val() as Project) ?? null)
    })

    return () => unsubscribe()
  })

  return {
    subscribe,
    set: (value: any) => {
      return set(projectRef, value)
    },
    update: () => {},
  }
}

export const project = realtimeStore('projectPath')

And in my components I can do stuff like:

<script lang="ts">
  import { project } from '$lib/firebase'

  $project.name = 'new name'
</script>

The problem comes when I need $project to start pointing to another path, coming from $page.params.projectId. I tried with a derived store, but it’s read only:

// $lib/firebase.ts
export const project: Readable<Project | null> = derived([selectedProjectId], ([$selectedProjectId], set) => {
  if ($selectedProjectId) {
    return realtimeStore<Project>($selectedProjectId).subscribe(set)
  } else {
    set(null)
  }
})

// If I try to do $project.name = '...' from a component 
// I get an error saying that project doesn't have a "set" function - 
// the store is read only.

What is the best way to have a writable $project that can also change the path to the realtime object?

2

Answers


  1. Chosen as BEST ANSWER

    As @parmer_110 suggested, added a setPath function to realtimeStore:

    // $lib/firebase
    export function realtimeStore<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)
          })
        },
      }
    }
    
    // Extend the Writable interface to make TypeScript happy
    interface TRealtimeDBWritable<T> extends Writable<T> {
      setPath: (path: string) => void
    }
    
    // Define our store
    export const artboardStore: TRealtimeDBWritable<Artboard | null> = realtimeStore<TArtboard>()
    

    To use the store:

    // MyComponent.svelte
    import { artboardStore } from '$lib/firebase'
    
    artboardStore.setPath('path/to/artboard/1')
    $artboardStore.name = 'new name'
    
    // later...
    
    artboardStore.setPath('path/to/artboard/2')
    $artboardStore.name = 'new name for other artboard'
    

  2. In Svelte, the stores using the writable function are read-write stores, whereas the stores created using the derived function are read-only. For writable store change the path to the Firebase Realtime object.

    // $lib/firebase.ts
    import { writable, onValue } from 'svelte/store'
    import { realtimeDB, ref, onCleanup } from './firebase' // Import necessary Firebase modules
    
    export function createRealtimeStore<Project>(path: string) {
      let unsubscribe: () => void
    
      const projectRef = ref(realtimeDB, path)
      const { subscribe, set, update } = writable<Project | null>(null, (set) => {
        unsubscribe = onValue(projectRef, (snapshot) => {
          set((snapshot.val() as Project) ?? null)
        })
    
        return () => unsubscribe()
      })
    
      return {
        subscribe,
        set,
        update,
        unsubscribe: () => {
          unsubscribe()
          onCleanup(() => {
            // Cleanup any other resources if needed
          })
        }
      }
    }
    
    export const project = createRealtimeStore('projectPath')
    

    Now, your project store is writable and can be updated using the set and update methods. Then you can change the path by unsubscribing from to the old path and subscribing to the new path. Update the imports and any other necessary code related to Firebase in the firebase.ts file.

    // $lib/firebase.ts
    export const project = createRealtimeStore<Project | null>('')
    

    In your component, you can use the $project store and update its value accordingly:

    <script lang="ts">
      import { project } from '$lib/firebase'
      import { onDestroy } from 'svelte'
    
      let unsubscribe: () => void
    
      // Update the project path whenever $page.params.projectId changes
      $: {
        unsubscribe?.()
        if ($page.params.projectId) {
          unsubscribe = project.unsubscribe()
          project.setPath($page.params.projectId)
        }
      }
    
      onDestroy(() => {
        unsubscribe?.()
      })
    
      // Update the project name
      $project.update(project => ({ ...project, name: 'new name' }))
    </script>
    

    Above, whenever $page.params.projectId changes, it unsubscribes from the old path and sets the new path using the setPath method (which you need to add to your createRealtimeStore function). The unsubscribe function is called on component destruction to clean up any active subscriptions.

    Update

    // $lib/firebase.ts
    import { writable, onValue } from 'svelte/store'
    import { realtimeDB, ref, onCleanup } from './firebase' // Import necessary Firebase modules
    
    export function createRealtimeStore<Project>(initialPath: string) {
      let currentPath = initialPath
      let unsubscribe: (() => void) | null = null
    
      const { subscribe, set, update } = writable<Project | null>(null, (set) => {
        const projectRef = ref(realtimeDB, currentPath)
    
        unsubscribe = onValue(projectRef, (snapshot) => {
          set((snapshot.val() as Project) ?? null)
        })
    
        return () => unsubscribe && unsubscribe()
      })
    
      return {
        subscribe,
        set,
        update,
        setPath: (newPath: string) => {
          if (currentPath !== newPath) {
            unsubscribe && unsubscribe()
            currentPath = newPath
            set(null) // Clear the store while the new path is being set up
          }
        },
        unsubscribe: () => {
          unsubscribe && unsubscribe()
          onCleanup(() => {
            // Cleanup any other resources if needed
          })
        }
      }
    }
    
    export const project = createRealtimeStore<Project | null>('')
    

    The projectRef is created inside the writable callback function,

    When the setPath method is called with a new path, it first unsubscribes from the previous path, updates the currentPath value, and clears the store by calling set(null). The store will then be updated with the new data once the new path is set up.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search