Vue.js 3 Firestore Basics Tutorial

In this Vue tutorial we learn how to interact with our Firestore database.

We cover how no-sql databases work, collections and documents and how to create, read, update and delete data from them.

Finally, we cover how to register realtime listeners and how to subscribe to and unsubscribe from them.

Lesson Video

If you prefer to learn visually, you can watch this lesson in video format.

Lesson Project

If you want to follow along with the examples, you will need to create an app generated by the Vue CLI . You will also need to have the Firebase SDK installed , as well as a Firestore database set up .

If you are continuing from the previous lesson, you should already be set up ready to go.

How a Firestore database works

A firestore database works on the concept of collection and documents instead of tables, rows and columns like traditional relational databases such as MySQL.

Let’s take a quick look at how it works.

Database

A database is the container for all our data. It contains the collections (with their documents) and each database gets its own set of files on the system.

Collection

A collection is a group of documents. We can think of it as the equivalent of a MySQL table.

A collection can only contain documents. It can’t contain other collections or raw JSON data.

Document

A document is a JSON object literal with sets of key:value pairs that contain our individual data entries.

A document can have a dynamic schema, which means that documents in the same collection do not neccesarily need to have the same set of fields, or structure.


How Firebase works

How to create a collection

Collections (and documents) are created implicitly in Firebase. We simply assign the data we want to store in a document to a collection we specify, and both will be created.

We have multiple options to create a document (and therefore collection) and add data to it.

How to create a document with addDoc

The addDoc method will create a new document in a collection with data we specify. If the collection doesn’t exist, it will automatically be created.

Because addDoc automatically generates a unique document ID, we use it when we don’t have a meaningful name or ID for a document.

The method takes two arguments.

  1. A collection reference .
  2. An object of data we want to store.
Syntax: addDoc
addDoc(collectionReference, dataObject)

The collection reference is a method that also takes two arguments.

  1. The configured Firestore instance.
  2. A string name of the collection we want to store to. If the collection doesn’t exist, it will be created automatically.
Syntax: collection
collection(firestoreInstance, 'collectionName')

As an example, let’s create a method in our root App component that adds some user details to a “users” collection. We’ll run the method in the created lifecycle hook .

tip Remember that HTTP requests return a promise, so we need to mark the method with async to be able to use await on the request.

Example: src/App.vue
<template>
  <div></div>
</template>

<script>
// the relevant methods
import { collection, addDoc } from "firebase/firestore"
// the firestore instance
import db from './firebase/init.js'

export default {
  created() {
    this.createUser()
  },
  methods: {
    async createUser() {
      // 'users' collection reference
      const colRef = collection(db, 'users')
      // data to send
      const dataObj = {
        firstName: 'John',
        lastName: 'Doe',
        dob: '1990'
      }

      // create document and return reference to it
      const docRef = await addDoc(colRef, dataObj)

      // access auto-generated ID with '.id'
      console.log('Document was created with ID:', docRef.id)
    }
  }
}
</script>

If we run the example in the browser and take a look in the dev tools console, we will see the auto-generated ID. If we open the database in the browser, we’ll see the new collection and a single new document.


Users collection with a single document

If we change the data and refresh the page, it will create a second document with the new data.

Example: src/App.vue
<template>
  <div></div>
</template>

<script>
import { collection, addDoc } from "firebase/firestore"
import db from './firebase/init.js'

export default {
  created() {
    this.createUser()
  },
  methods: {
    async createUser() {
      const colRef = collection(db, 'users')
      const dataObj = {
        firstName: 'Jane',
        lastName: 'Doe',
        dob: '1991'
      }

      const docRef = await addDoc(colRef, dataObj)
      console.log('Document was created with ID:', docRef.id)
    }
  }
}
</script>

note If we refresh the page with the same data, a third document will be created. The addDoc method doesn’t overwrite documents with the same data.

How to create a document with setDoc

The setDoc method works almost the same as addDoc , except we need to specify a unique ID for the document explicitly.

The method takes two arguments.

  1. A document reference
  2. An object of data we want to store.
Syntax: setDoc
setDoc(documentReference, dataObject)

The document reference is a method that takes three arguments.

  1. The configured Firestore instance.
  2. A string name of the collection we want to store to. If the collection doesn’t exist, it will be created automatically.
  3. A unique document ID or name.
Syntax: collection
doc(firestoreInstance, 'collectionName', 'documentID')

As an example, let’s change the method in our root App component to add countries to a “countries” collection. We’ll run the method in the created lifecycle hook .

This time, we’ll do it all in one statement. We’ll also remove the console log because we already know the ID.

Example: src/App.vue
<template>
  <div></div>
</template>

<script>
// the relevant methods
import { doc, setDoc } from "firebase/firestore"
import db from './firebase/init.js'

export default {
  created() {
    this.createCountry()
  },
  methods: {
    async createCountry() {

      await setDoc(doc(db, 'countries', 'US'), {
        name: 'United States'
      })
    }
  }
}
</script>

If we run the example in the browser, then take a look in the database, we’ll see the new collection and it’s document.


Countries collection with a single document

Changing the data and refreshing the page will create another document.

Example: src/App.vue
<template>
  <div></div>
</template>

<script>
import { doc, setDoc } from "firebase/firestore"
import db from './firebase/init.js'

export default {
  created() {
    this.createCountry()
  },
  methods: {
    async createCountry() {

      await setDoc(doc(db, 'countries', 'GB'), {
        name: 'Great Britain'
      })
    }
  }
}
</script>

note If we refresh the page with the same data, it will overwrite the document that contains the same data unless we explicitly specify we want to merge data.

How to add/merge new data with setDoc

To merge data into an existing document, we specify a third parameter to the setDoc method.

The parameter is an options object with the key called merge set to true.

Syntax: merge
setDoc(collectionReference, dataObject, { merge: true })

As an example, let’s merge some new data to our “GB” document.

Example: src/App.vue
<template>
  <div></div>
</template>

<script>
import { doc, setDoc } from "firebase/firestore"
import db from './firebase/init.js'

export default {
  created() {
    this.addCountryCapital()
  },
  methods: {
    async addCountryCapital() {

      await setDoc(doc(db, 'countries', 'GB'), {
        // new data
        capital: 'London'
        // merge
      }, { merge: true })
    }
  }
}
</script>

If we run the example in the browser, then take a look at the database, we’ll see the new entry in the “GB” document.

tip Although documents in a collection typically have the same structure, it’s not a requirement. For example, the capital entry in the “GB” document doesn’t need to exist in the “US” document if we don’t need it. This allows our database to be leaner and consequently, faster.

How to update existing data in a document with updateDoc

We can update existing data with the updateDoc method.

The method takes two arguments.

  1. A document reference.
  2. An object with the data we want to update.
Syntax: updateDoc
updateDoc(documentReference, dataObject)

As an example, let’s change the capital in our “GB” document.

Example: src/App.vue
<template>
  <div></div>
</template>

<script>
// relevant methods
import { doc, updateDoc } from "firebase/firestore"
import db from './firebase/init.js'

export default {
  created() {
    this.updateCountry()
  },
  methods: {
    async updateCountry() {

      await updateDoc(doc(db, 'countries', 'GB'), {
        capital: 'Londinium'
      })
    }
  }
}
</script>

If we run the example in the browser, then take a look at the “GB” document in the database, it will show the updated value.

How to fetch a document with getDoc

We can fetch a Firebase document with the getDoc method.

The method takes a single argument.

  1. The document reference we want to fetch.
Syntax: getDoc
getDoc(documentReference)

The data is returned as an object into a variable or constant and accessed with the data method.

Syntax: data
snapshot = getDoc(documentReference)

snapshot.data()       // full object
snapshot.data().field // single field (key)

We can also use the exists method to check if the document we’re trying to fetch exists.

Syntax: exists
snapshot = getDoc(documentReference)

if (snapshot.exists()) {
  // document exists, use it
} else {
  // document does not exist
  // .data() will be undefined
}

As an example, let’s fetch our “GB” document and log its contents to the console.

Example: src/App.vue
<template>
  <div></div>
</template>

<script>
// relevant methods
import { doc, getDoc } from "firebase/firestore"
import db from './firebase/init.js'

export default {
  created() {
    this.getCountry()
  },
  methods: {
    async getCountry() {

      const docSnap = await getDoc(doc(db, 'countries', 'GB'))

      if (docSnap.exists()) {
        console.log(docSnap.data())
      } else {
        console.log('Document does not exist')
      }

    }
  }
}
</script>

As mentioned earlier, we use .data().field to access individual keys in the object. Let’s assign the firestore data to local data properties and output them in the template.

Example: src/App.vue
<template>
  <p>
    Country: {{ name }}<br>
    Capital: {{ capital }}
  </p>
</template>

<script>
import { doc, getDoc } from "firebase/firestore"
import db from './firebase/init.js'

export default {
  data() {
    return {
      name: '',
      capital: ''
    }
  },
  created() {
    this.getCountry()
  },
  methods: {
    async getCountry() {

      const docSnap = await getDoc(doc(db, 'countries', 'GB'))

      if (docSnap.exists()) {
        // assign document fields
        // to data properties
        this.name = docSnap.data().name
        this.capital = docSnap.data().capital
      } else {
        console.log('Document does not exist')
      }

    }
  }
}
</script>

How to fetch multiple documents with getDocs

We can fetch multiple Firebase documents with the getDocs method.

The method takes a single argument.

  1. The query of which documents we want to fetch.
Syntax: getDocs
getDocs(query)

The query is a method that takes two arguments.

  1. A collection reference.
  2. [Optional] Query filter methods.
Syntax: collection
query(collectionReference, queryFilter())

As an example, let’s fetch all the documents in the “countries” collection and output them in the template.

We’ll store the firestore objects in a local data array and loop over the array in the template.

Example: src/App.vue
<template>
  <p v-for="country in countries" :key="country.name">
    Country: {{ country.name }}<br>
    Capital: {{ country.capital }}
  </p>
</template>

<script>
import { query, collection, getDocs } from "firebase/firestore"
import db from './firebase/init.js'

export default {
  data() {
    return {
      countries: []
    }
  },
  created() {
    this.getCountry()
  },
  methods: {
    async getCountry() {
      // query to get all docs in 'countries' collection
      const querySnap = await getDocs(query(collection(db, 'countries')));

      // add each doc to 'countries' array
      querySnap.forEach((doc) => {
        this.countries.push(doc.data())
      })

    }
  }
}
</script>

How to query for specific documents with where

We can search for documents matching certain criteria with the where method in the query ’s second argument.

This method takes three arguments.

  1. The key we want to search in.
  2. The evaluation operator.
  3. The value we want to filter by.
Syntax: where
where('key', operator, 'filter')

For example, if we want to find the country that has the capital “Londinium”.

Example: where
where('capital', '==', 'Londinium')

tip JSON objects are not the same as Javascript objects, they are object literals and work with strings.

As a full example, let’s find all the users in our “users” collection born after 1990 and print them to the console.

Example: src/App.vue
<template>
  <p v-for="user in users" :key="user.firstName">
    {{ user.firstName }} {{ user.lastName }}
  </p>
</template>

<script>
import { where, query, collection, getDocs } from "firebase/firestore"
import db from './firebase/init.js'

export default {
  data() {
    return {
      users: []
    }
  },
  created() {
    this.getUsers()
  },
  methods: {
    async getUsers() {
      // filter to get users with 'dob' after 1990
      const q = query(collection(db, 'users'), where('dob', '>', '1990'))
      const querySnap = await getDocs(q);

      querySnap.forEach((doc) => {
        this.users.push(doc.data())
      })

    }
  }
}
</script>

The official documentation covers more queries as well as the full list of evaluation operators available.

How to order documents by a specific field with orderBy

Firebase allows us to easily order documents by a field in ascending and descending order with the orderBy filter method.

This method takes two arguments.

  1. The field to order by.
  2. [Optional] The abbreviated order (“asc” or “desc”) to order by.
Syntax: orderBy
orderBy('field', 'order')

tip The default order is “asc” so we may omit it if we want the results in ascending order.

As an example, let’s fetch our two users and order them by first name.

Example: src/App.vue
<template>
  <p v-for="user in users" :key="user.firstName">
    {{ user.firstName }} {{ user.lastName }}
  </p>
</template>

<script>
import { query, collection, getDocs, orderBy  } from "firebase/firestore"
import db from './firebase/init.js'

export default {
  data() {
    return {
      users: []
    }
  },
  created() {
    this.getUsers()
  },
  methods: {
    async getUsers() {
      // order users by name (alphabetical order)
      const q = query(collection(db, 'users'), orderBy('firstName'))
      const querySnap = await getDocs(q);

      querySnap.forEach((doc) => {
        this.users.push(doc.data())
      })

    }
  }
}
</script>

We can combine ordering with other filters like where . In that case, we simply separate them with a comma.

Example:
query(collection(db, 'users'), orderBy('firstName'), where('dob', '>', '1990'))

How to limit the number of fetched documents with limit

To limit the number of documents we fetch per query, we use the limit method.

This method takes a single argument.

  1. The number of documents we want to limit by.
Syntax: limit
limit(number)

As an example, let’s limit our earlier example to only show one user.

Example: src/App.vue
<template>
  <p v-for="user in users" :key="user.firstName">
    {{ user.firstName }} {{ user.lastName }}
  </p>
</template>

<script>
import { query, collection, getDocs, orderBy, limit } from "firebase/firestore"
import db from './firebase/init.js'

export default {
  data() {
    return {
      users: []
    }
  },
  created() {
    this.getUsers()
  },
  methods: {
    async getUsers() {
      // limit the ordered users to 1
      const q = query(collection(db, 'users'), orderBy('firstName'), limit(1))
      const querySnap = await getDocs(q);

      querySnap.forEach((doc) => {
        this.users.push(doc.data())
      })

    }
  }
}
</script>

note The SDK has specific methods if you want to paginate data .

How to delete a document with deleteDoc

To delete a Firebase document, we use the deleteDoc method.

The method takes a single argument.

  1. A document reference.
Syntax: deleteDoc
deleteDoc(documentReference)

As an example, let’s delete the “US” document in the “countries” collection.

Example: src/App.vue
<template>
  <div></div>
</template>

<script>
import { doc, deleteDoc } from 'firebase/firestore'
import db from './firebase/init.js'

export default {
  created() {
    this.dropCountry()
  },
  methods: {
    async dropCountry() {
      await deleteDoc(doc(db, 'countries', 'US'))
    }
  }
}
</script>

If we run the example in the browser, then take a look at the “countries” collection in the database, the “US” document will be gone.

How to delete a field from a document with deleteField

We can delete individual fields from a document be specifying the deleteField method in updateDoc.

The method takes no arguments.

Syntax: deleteField
updateDoc(documentReference, {
    field: deleteField()
})

As an example, let’s delete the “capital” field from the “GB” document in the “countries” collection.

Example: src/App.vue
<template>
  <div></div>
</template>

<script>
import { doc, updateDoc, deleteField } from 'firebase/firestore'
import db from './firebase/init.js'

export default {
  created() {
    this.dropCapital()
  },
  methods: {
    async dropCapital() {
      await updateDoc(doc(db, 'countries', 'GB'), {
        capital: deleteField()
      })
    }
  }
}
</script>

If we run the example in the browser, then take a look at the “US” document in the database, the “capital” field will be gone.

How to delete a collection

The official documentation recommends not deleting collections from a web-based client for performance and security reasons.

Realtime listeners

Realtime listeners allow our apps to automatically update when data changes or is added to the database.

For example, let’s say we have an app that shows the latest films available at the cinema in a user’s area. If a user is viewing the list while a new film is added, the page will automatically change to show the new film in the list.

To demonstrate the default behavior, let’s create a button that adds a capital to a document in our “countries” collection and output it to the template.

Example: src/App.vue
<template>
  <p>
    Country: {{ name }}<br>
    Capital: {{ capital }}
  </p>
  <button @click="addCountryCapital">Add Capital</button>
</template>

<script>
import { doc, setDoc, getDoc } from 'firebase/firestore'
import db from './firebase/init.js'

export default {
  data() {
    return {
      name: '',
      capital: ''
    }
  },
  created() {
    this.getCountry()
  },
  methods: {
    async getCountry() {
      const docSnap = await getDoc(doc(db, 'countries', 'GB'))

      if (docSnap.exists()) {
        this.name = docSnap.data().name
        this.capital = docSnap.data().capital
      }
    },
    async addCountryCapital() {
      // add capital to country
      await setDoc(doc(db, 'countries', 'GB'), {
        capital: 'London'
      }, { merge: true })
    }
  }
}
</script>

If we run the example in the browser and click on the button, nothing will show for the capital. But if we take a look in the database under the “GB” document, it exists.

We will have to refresh the page to see the capital.

How to register a realtime listener on a single document

To register a realtime listener on a document, we use the onSnapshot method.

The method takes three arguments.

  1. A document reference.
  2. A callback function to handle the data.
  3. [Optional] A callback function to handle listen errors.
Syntax: onSnapshot
onSnapshot(
  docReference,
  (snapshot) => {
    // handle data
  },
  (error) => {
    // handle error
  }
)

To demonstrate, let’s change our earlier example and add an “Also Known As” when we click the button.

Example: src/App.vue
<template>
  <p>
    Country: {{ name }} (aka. {{ aka }})<br>
    Capital: {{ capital }}
  </p>
  <button @click="addAlsoKnownAs">Add AKA</button>
</template>

<script>
import { doc, setDoc, onSnapshot } from 'firebase/firestore'
import db from './firebase/init.js'

export default {
  data() {
    return {
      aka: '',
      name: '',
      capital: ''
    }
  },
  created() {
    this.getCountry()
  },
  methods: {
    async getCountry() {
      // register realtime listener
      // for changes on 'GB' document
      onSnapshot(doc(db, 'countries', 'GB'), (snap) => {
        this.aka = snap.data().aka
        this.name = snap.data().name
        this.capital = snap.data().capital
      })
    },
    async addAlsoKnownAs() {
      // add 'aka' field to document
      await setDoc(doc(db, 'countries', 'GB'), {
        aka: 'United Kingdom'
      }, { merge: true })
    }
  }
}
</script>

This time when we click on the button, the text in the template immediately updates with the new field value.

How to register a realtime listener on a collection

To register a realtime listener on a collection, we use a collection reference instead of a document reference.

Syntax: onSnapshot
onSnapshot(
  collectionReference,
  (snapshot) => { },
  (error) => { }
)

To demonstrate, let’s add a realtime listener on our “users” collection and output each user in a paragraph in the template.

Example: src/App.vue
<template>
  <p v-for="user in users" :key="user.firstName">
    {{ user.firstName }} {{ user.lastName }}
  </p>
</template>

<script>
import { collection, onSnapshot } from 'firebase/firestore'
import db from './firebase/init.js'

export default {
  data() {
    return {
      users: []
    }
  },
  created() {
    this.getUsers()
  },
  methods: {
    async getUsers() {
      // use 'collection()' instead of 'doc()'
      onSnapshot(collection(db, 'users'), (snap) => {

        snap.forEach((doc) => {
          this.users.push(doc.data())
        })
      })
    }
  }
}
</script>

If we run the example in the browser, both users show. If we added another user, it would immediately update like the document example did.

How to register a realtime listener on query results

To register a realtime listener on the results of a query, we use a query instead of a document/collection reference.

Syntax: onSnapshot
onSnapshot(
  query,
  (snapshot) => { },
  (error) => { }
)

To demonstrate, let’s change our previous example to use a query that only returns users born after 1990.

tip Remember that the query method still needs a collection reference.

Example: src/App.vue
<template>
  <p v-for="user in users" :key="user.firstName">
    {{ user.firstName }} {{ user.lastName }}
  </p>
</template>

<script>
import { query, where, collection, onSnapshot } from 'firebase/firestore'
import db from './firebase/init.js'

export default {
  data() {
    return {
      users: []
    }
  },
  created() {
    this.getUsers()
  },
  methods: {
    async getUsers() {

      onSnapshot(
        // use 'query()' instead of a reference
        query(collection(db, 'users'), where('dob', '>', '1990')),
        (snap) => {
          snap.forEach((doc) => {
            this.users.push(doc.data())
        })
      })
    }
  }
}
</script>

If we run the example in the browser, the only user born after 1990 will show. If we added another user with a “dob” above 1990 to the database, it will update in real time like the other examples.

How to unsubscribe from a realtime listener

The onSnapshot method returns a method that unsubscribes the realtime listener. So we can assign it to a constant and later invoke the method with that constant where we want to unsubscribe.

Syntax: onSnapshot unsubscribe
const unsub = onSnapshot(
  reference/query,
  (snapshot) => { },
  (error) => { }
)

// later...
unsub()

Further Reading

For more information on the topics covered in this lesson, please see the relevant section below.