Binding / Subscribing to changes

In Vuexfire, 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 as well as the Source (Collection, Query or Document) and Vuexfire takes care of the rest!

Once a Reference is bound, Vuexfire will keep the local version in sync with the remote database. However, this synchronisation is only one-way, the local state is always a reflection of the remote Database that you should treat as read only state. If you want to push changes to the remote Database, you need to use Firebase JS SDK. But, more about that later.

Binding in actions

The right place to bind references is in actions. This will also simplify your testing strategy if you choose to mock the store with something like vuex-mock-store and also keep writes in actions, you won't have to worry about mocking Firebase database at all in most scenarios. Because of this, Vuexfire provides an action wrapper that injects into the context (first parameter of actions) two new functions: bindFirestoreRef/bindFirebaseRef and unbindFirestoreRef/unbindFirebaseRef. That way you can bind a reference as well as unbind existing bindings directly in actions:

// store.js
import Vuex from 'vuex'
import { vuexfireMutations, firestoreAction } from 'vuexfire'
import { db } from './db'

export default new Vuex.Store({
  state: {
    todos: [],
  },

  mutations: vuexfireMutations,

  actions: {
    bindTodos: firestoreAction(({ bindFirestoreRef }) => {
      // return the promise returned by `bindFirestoreRef`
      return bindFirestoreRef('todos', db.collection('todos'))
    }),
  },
})

It's necessary to declare properties with their initial values in state. 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.

Always return or await the promise returned by bindFirestoreRef/bindFirebaseRef since it let you know when your state is filled with data coming from the database. This is indeed useful when dealing with SSR

Unbinding

To stop the state to be in sync, you can manually do so with unbindFirestoreRef/unbindFirebaseRef:

// store.js
export default new Vuex.Store({
  // other store options are omitted for simplicity reasons

  actions: {
    unbindTodos: firestoreAction(({ unbindFirestoreRef }) => {
      unbindFirestoreRef('todos')
    }),
  },
})

When unbinding, there is no need to wait for a promise, all listeners are teared down. By default, data will be reset, you can customize by providing a second argument to the unbindFirebaseRef/unbindFirestoreRef function:

// store.js
export default new Vuex.Store({
  // other store options are omitted for simplicity reasons

  actions: {
    someAction: firestoreAction(({ state, bindFirestoreRef, unbindFirestoreRef }) => {
      unbindFirestoreRef('todos')
      // or
      unbindFirestoreRef('todos', true)
      // state.todos === []

      // using the boolean version
      unbindFirestoreRef('todos', false)
      // state.todos === [{ text: 'Use Firestore Refs' }]

      // using the function syntax
      unbindFirestoreRef('todos', () => [{ text: 'placeholder' }])
      // state.todos === [{ text: 'placeholder' }]

      // documents are reset to null instead, you can also provide the same options as above
      unbindFirestoreRef('doc')
      // state.doc === null
    }),
  },
})

Binding over existing bindings

When calling bindFirestoreRef/bindFirebaseRef to bind a collection or document over an existing binding, it isn't necessary to call unbindFirestoreRef/unbindFirebaseRef, it's automatically done for you.

By default, values are reset but this behaviour can be customized with the reset option:

// store.js
export default new Vuex.Store({
  // other store options are omitted for simplicity reasons

  actions: {
    someAction: firestoreAction(({ state, bindFirestoreRef, unbindFirestoreRef }) => {
      // will keep the previous value
      bindFirestoreRef('todos', { reset: false })
    }),
  },
})

Using the data bound by Vuexfire

.key / id

Any document bound by Vuexfire 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 only copy the data using the spread operator or Object.assign.

store.state.user.id // jORwjIykFn1NmkdzTkhU
// the id is non enumerable
Object.keys(store.state.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. They are retrieved as-is by Vuexfire, meaning that you can directly use methods like isEqual and access its properties latitude and longitude.

Refer to Easy access to Firebase database 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 = store.state.cities[this.cities.length - 1]
paris.location.latitude // 48.8588377
paris.location.longitude // 2.2770206

Timestamps (Firestore only)

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

Refer to Easy access to Firebase database 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 = store.state.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 to other Documents in Documents. Vuexfire automatically bind References found in Collections and documents. This also works for nested references (References found in bound References). By default, Vuexfire 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 Vuexfire, 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 Vuexfire 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
bindFirestoreRef('user', db.collection('users').doc('1'), { maxRefDepth: 1 })

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