# Binding / Subscribing to changes

In Vuefire, subscriptions to changes are handled transparently. That's why we always talk about binding: you only provide the key of the state where to bind, and the Source (Collection, Query or Document), and Vuefire takes care of the rest!

There are two ways of binding a Reference to the Database with Vuefire:

  • Declarative binding with the firebase/firestore option
  • Programmatic binding with the injected methods $rtdbBind/$bind

Once a Reference is bound, Vuefire will keep the local version synchronized with the remote Database. However, this synchronisation is only one-way. Do not modify the local variable (e.g. doing this.user.name = 'John'), because (a) it will not change the remote Database and (b) it can be overwritten at any time by Vuefire. To write changes to the Database, use the Firebase JS SDK.

# Declarative binding

Any Database Reference provided in a firebase/firestore option will be bound at creation (after Vue's created hook) to the specified key on the component. In the following example we bind a Collection of Documents to our documents property. The key provided in the firebase/firestore option (documents) must be initialized in the data of the component:

// RecentDocuments.vue
import { db } from './db'

export default {
  data() {
    return {
      documents: [],
    }
  },

  firestore: {
    documents: db.collection('documents'),
  },
}

WARNING

You must declare properties with their initial values in data. For the RTDB, using an Array as the initial value will bind the Reference as an array, otherwise it is bound as an object. For Firestore, collections and queries are bound as arrays while documents are bound as objects.

# Programmatic binding

If you need to change the bound reference while the application is running, e.g. to display a different user profile, or different product detail page, Declarative binding isn't enough. This can be achieved through the $rtdbBind/$bind methods added by rtdbPlugin/firestorePlugin in any Vue component.

// UserProfile.vue
const users = db.collection('users')

export default {
  props: ['id'],
  data() {
    return {
      user: null,
    }
  },

  watch: {
    id: {
      // call it upon creation too
      immediate: true,
      handler(id) {
        this.$bind('user', users.doc(id))
      },
    },
  },
}

With the approach above, user will always be bound to the user defined by the prop id

TIP

No need to call $rtdbUnbind/$unbind as $rtdbBind/$bind will automatically unbind any existing binding on the provided key. Upon component removal, all bindings are removed as well, so no need to use $rtdbUnbind/$unbind in destroyed hooks.

If you need to wait for a binding to be ready before doing something, you can await for the returned Promise:

this.$bind('user', users.doc(this.id)).then(user => {
  // user will point to the same property declared in data:
  // this.user === user
})

this.$bind('documents', documents.where('creator', '==', this.id)).then(documents => {
  // documents will point to the same property declared in data:
  // this.documents === documents
})

# Using the data bound by Vuefire

# .key / id

Any document bound by Vuefire will retain it's id in the Database as a non-enumerable, read-only property. This makes it easier to write changes and allows you to copy the data only using the spread operator (opens new window) or Object.assign (opens new window).

this.user.id // jORwjIykFn1NmkdzTkhU
// the id is non enumerable
Object.keys(this.user).includes('id') // false

// it originally comes from the `id` attribute
db.collection('users').doc('ada').id // 'ada'
// More at https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#key
// https://firebase.google.com/docs/reference/js/firebase.database.Reference#key

# Geopoints (Firestore only)

In Firestore you can store Geopoints (opens new window). They are retrieved as-is by Vuefire, meaning that you can directly use methods like isEqual and access its properties latitude and longitude.

Refer to Plugin installation to retrieve the Geopoint class

import { GeoPoint } from './db'

// add Paris to the list of cities and wait for the operation
// to be finished
await db.collection('cities').add({
  name: 'Paris',
  location: new GeoPoint(48.8588377, 2.2770206),
})

// we consider `cities` to be bound to current component
// we retrieve Paris that was just added
const paris = this.cities[this.cities.length - 1]
paris.location.latitude // 48.8588377
paris.location.longitude // 2.2770206

# Timestamps (Firestore only)

In Firestore you can store Timestamps (opens new window). They are stored as-is by Vuefire, meaning that you can directly use methods like isEqual, toDate and access its properties seconds and nanoseconds.

Refer to Plugin installation to retrieve the Timestamp class

import { Timestamp } from './db'

// Add "La prise de la Bastille" to a list of events
// and wait for th operation to be finished
await db.collection('events').add({
  name: 'Prise de la Bastille',
  date: Timestamp.fromDate(new Date('1789-07-14')),
})

// we consider `events` to be bound to current component
// we retrieve the event we just added
const prise = this.events[this.events.length - 1]
prise.date.seconds // -5694969600
prise.date.nanoseconds // 0
prise.toDate() // Tue Jul 14 1789

# References (Firestore only)

In Firestore you can store References (opens new window) to other Documents in Documents. Vuefire automatically bind References found in Collections and documents. This also works for nested references (References found in bound References). By default, Vuefire will stop at that level (2 level nesting).

Given some users with documents that are being viewed by other users. This could be users/1:

{
  name: 'Jessica',
  documents: [
    db.collection('documents').doc('gift-list'),
  ],
}

documents is an array of References. Let's look at the document identified by gift-list:

{
  content: '...',
  sharedWith: [
    db.collection('users').doc('2'),
    db.collection('users').doc('3'),
  ]
}

sharedWith is also an array of References, but those references are users. Users also contain references to documents, therefore, if we automatically bind every nested reference, we could end up with an infinite-memory-consumming binding. By default, if we bind users/1 with Vuefire, this is what we end up having:

{
  name: 'Jessica',
  documents: [
    {
      content: '...',
      sharedWith: [
        {
          name: 'Alex',
          documents: [
            'documents/alex-todo-list',
          ]
        },
        {
          name: 'Robin',
          documents: [
            'documents/robin-todo-list',
            'documents/robin-book',
          ],
        },
      ],
    },
  ],
}

documents.sharedWith.documents end up as arrays of strings. Those strings can be passed to db.doc() as in db.doc('documents/robin-book') to get the actual reference to the document. By being a string instead of a Reference, it is possibe to display a bound document with Vuefire as plain text.

It is possible to customize this behaviour by providing a maxRefDepth option when invoking $bind:

// override the default value of 2 for maxRefDepth
this.$bind('user', db.collection('users').doc('1'), { maxRefDepth: 1 })

Read more about writing References to the Database in the writing data section.

# Unbinding / Unsubscribing to changes

While Vuefire will automatically unbind any reference bound in a component whenever needed, you may still want to do it on your own to stop displaying updates on a document or collection or because the user logged out and they do not have read-access to a resource anymore.

// unsubscribe from Database updates
this.$unbind('user')
this.$unbind('documents')

By default, Vuefire will reset the property, you can customize this behaviour by providing a second argument to the unbind/rtdbUnbind

// default behavior
this.$unbind('user')
this.$unbind('user', true)
// this.user === null

// using a boolean value for reset to keep current value
this.$unbind('user', false)
// this.user === { name: 'Eduardo' }

// using the function syntax to customize the value
this.$unbind('user', () => ({ name: 'unregistered' }))
// this.user === { name: 'unregistered' }

// for collections, they are reset to an empty array by default instead of `null`
this.$unbind('documents')
// this.documents === []

It's also possible to customize this behavior when binding by using the reset option:

// using a boolean value for reset
await this.$bind('user', userRef)
this.$bind('user', otherUserRef, { reset: false })
// while the user is fetched
// this.user === { name: 'Eduardo' }