Composables
The project utilises the following composablse which we will dissect now:
- getUser
- useSignup
- useLogin
- useLogout
- getCollection
- useCollection
- getDocument
- useDocument
- useStorage
getUser
import { ref } from "@vue/reactivity"
import { fAuth } from "../firebase/config"
const user = ref(fAuth.currentUser)
fAuth.onAuthStateChanged(_user => {
user.value = _user
})
const getUser = () => {
return { user }
}
export default getUser
Ref and firebase auth are imported, a const user
is created as a ref and the value of the current user is assigned to user. If there is a current user logged in the value of user is the user object, if not the value is null
.
Then a firebase auth method is used to add a listener for any changes in user auth and re-assigns the value of user
on every update
the getUser()
function is exported, which has access to user
through closure. user
is returned when getUser is destructed.
This composable is used any time we want to check user auth for say route guarding, adding a document to a collection, giving the user access to their own collection, logging a user in etc.
useSignup
import { ref } from "@vue/reactivity"
import { fAuth } from "../firebase/config"
const error = ref(null)
const isPending = ref(false)
const signup = async (email, password, displayName) => {
error.value = null
isPending.value = true
try{
const res = await fAuth.createUserWithEmailAndPassword(email, password)
if(!res){
throw new Error('Could not complete the signup, please try again')
}
await res.user.updateProfile({ displayName })
error.value = null
isPending.value = false
return res
}
catch(err){
console.log(err.message)
error.value = err.message
isPending.value = false
}
}
const useSignup = () => {
return { error, signup, isPending }
}
export default useSignup
useSignup
is exported which returns properties isPending
and error
and also the signup()
function. when the signup()
is invoked it takes in the user inputs: email
, password
and userName
error is set to null and isPending to true to use the boolean to show any loading elements. signup()
is an asynchronous function as it is communicating with firebase and we use try
, catch
blocks to manage the response.
The return value of a successful response is saved to a const res
, which is ultimately returned. If firebase has successfully accepted and created a new user the user profile is updated with their displayName. Once this has been completed via the await
keyword the ispending
property is set back to false and res
is returned
useLogin
import { ref } from "@vue/reactivity"
import { fAuth } from "../firebase/config"
const error = ref(null)
const isPending = ref(false)
const login = async (email, password) => {
error.value = null
isPending.value = true
try{
const res = await fAuth.signInWithEmailAndPassword(email, password)
error.value = null
isPending.value = false
return res
}
catch(err) {
error.value = 'Incorrect login credentials'
isPending.value = false
}
}
const useLogin = () => {
return { error, login, isPending }
}
export default useLogin
Same structural pattern as previous, the login()
function takes in the user input: email
and password
. within the try
block await
is used ahead of calling firebase Auth and using the authentication method to attempt logging the user in. Once a successful response has been returned error is set to null and isPending to false, with the response res
being returned from login()
.
useLogout
import { ref } from "@vue/reactivity"
import { fAuth } from "../firebase/config"
const error = ref(null)
const isPending = ref(false)
const logout = async () => {
error.value = null
isPending.value = true
try{
await fAuth.signOut()
isPending.value = false
}
catch(err) {
error.value = err.message
isPending.value = false
console.log(error.value)
}
}
const useLogout = () => {
return { error, logout, isPending }
}
export default useLogout
Same pattern again, but using the the Firebase auth method signout()
getCollection
import { ref } from "@vue/reactivity"
import { watchEffect } from "@vue/runtime-core"
import { fStore } from "../firebase/config"
const getCollection = (collection, query) => {
const documents = ref(null)
const error = ref(null)
let collectionRef = fStore.collection(collection).orderBy('createdAt',"desc")
if(query){
collectionRef = collectionRef.where(...query)
}
const unsub = collectionRef.onSnapshot((snap) => {
let results = []
snap.docs.forEach(doc => {
doc.data().createdAt && results.push({ ...doc.data(), id: doc.id })
})
documents.value = results
error.value = null
}, (err) => {
console.log(err.message)
error.value = 'Could not fetch data'
documents.value = null
})
// unsub method to prevent multiple onSnapshot events running at the same time
watchEffect((onInvalidate) => {
onInvalidate(() => unsub())
})
return { documents, error }
}
export default getCollection
We also import watchEffect
here, similar to watch
, watchEffect
is a listener for updates to reactive properties and fires its callback every time there is an update. Unlike watch
, watchEffect
runs initially on load as well. we will come back to this.
getCollection
takes in two arguments: collection
and query
with query being some kind of filter to only grab specific documents within a collection that meet a criteria. documents
is initially null but is returned as an array of document objects from a collection.
First we make a reference to the target collection and we are also ordering the documents using the firestore method.
Then the if
statement checks to see if there is a query to return a filtered collection and if so chains the query onto the collection reference by using the where()
method and spreading the query in.
Below that we add create a const unsub
which we will come back to and add the firebase onSnapshot
listener to the collection ref which fires every time there is an update to a document.
Diving into the callback, which takes in the particular snapshot snap
, first the results array is set to empty, then a forEach
is used on the firebase doc.docs
to cycle through each document in the collection.
data()
is a firestore method to acces the document data and first there is a check for a createdAt
property from firestore to make sure the snapshot data is not the locally emitted data, then each docs data is spread into the results array. The id is added separately as this property is not in the document object data but on the document.
Then the results array is assigned as the value of documents
, with documents
being returned from getCollection
.
onSnapshot
listeners do not by default unsubscribe when the view un mounts, so to prevent them from building up on top of each other we stick the const unsub
in front of the onSnapshot
so we can invoke it and stop the listener.
This is done just above the returned object within the watchEffect
function, using onValidate()
.
useCollection
import { ref } from '@vue/reactivity'
import { fStore } from '../firebase/config'
const useCollection = (collection) => {
const error = ref(null)
const isPending = ref(false)
const addDoc = async (doc) => {
error.value = null
isPending.value = true
try{
const res = await fStore.collection(collection).add(doc)
isPending.value = false
return res
}
catch(err){
console.log(err.message)
isPending.value = false
error.value = 'Unable to send this message'
}
}
return { addDoc, error, isPending }
}
export default useCollection
Again the same composable structure. useCollection
takes in the collection as an argument when destructed and returns the addDoc()
function, which itself takes in an argument, the new document to be added to the collection.
Then simply using the firestore method add()
an attempt to add the document to the db is made. Again this is asynchronous so the keyword await
is used. The returned value is saved to a const res
, which is returned from the addDoc()
function.
getDocument
import { ref } from "@vue/reactivity"
import { watchEffect } from "@vue/runtime-core"
import { fStore } from "../firebase/config"
const getDocument = (collection, id) => {
const document = ref(null)
const error = ref(null)
let docRef = fStore.collection(collection).doc(id)
const unsub = docRef.onSnapshot((doc) => {
if(doc.data()){
document.value = {...doc.data(), id: doc.id}
error.value = null
} else{
error.value = 'That project does not exist'
}
}, (err) => {
console.log(err.message)
error.value = 'Could not fetch data'
document.value = null
})
// unsub method to prevent multiple onSnapshot events running at the same time
watchEffect((onInvalidate) => {
onInvalidate(() => unsub())
})
return { document, error }
}
export default getDocument
The getDocument
function takes in the collection and document id when it is invoked and destructed, and returns document
.
First a reference to the document is made docRef
, then a const unsub
(we have seen this before) is defined and used as before as a way of stopping the onSnapshot
listener. A listener is added to the document and if
the document has data (exists) the document data is assigned as the value of document
.
useDocument
import { ref } from '@vue/reactivity'
import { fStore } from '../firebase/config'
const useDocument = (collection, id) => {
let error = ref(null)
let isPending = ref(false)
let docRef = fStore.collection(collection).doc(id)
const deleteDoc = async () => {
isPending = true
error.value = null
try{
const res = await docRef.delete()
isPending = false
return res
}
catch(err){
error.value = 'Unable to delete the project'
isPending = false
console.log(err.message)
}
}
const updateDoc = async (updates) => {
isPending = true
error.value = null
try{
const res = await docRef.update(updates)
isPending = false
return res
}
catch(err){
error.value = 'Unable to update'
isPending = false
console.log(err.message)
}
}
return { updateDoc, deleteDoc, error, isPending }
}
export default useDocument
useDocument
takes in the collection and doc id when destructed and returns the deleteDoc
and updateDoc
functions.
the document reference docRef
is used in both functions which they have access to via closure.
The deleteDoc()
function takes no argument and simply uses the delete()
method.
The updateDoc()
function takes in an object including any properties and values to be updated. This function is used to update the project document when the tasks array is updated.
useStorage
import { ref } from "@vue/reactivity"
import { fStorage } from "../firebase/config"
import getUser from '@/composables/getUser'
const { user } = getUser()
const useStorage = () => {
const error = ref(null)
const url = ref(null)
const filePath = ref(null)
const uploadImage = async (file) => {
filePath.value = `projectImages/${user.value.uid}/${file.name}`
const storageRef = fStorage.ref(filePath.value)
try{
const res = await storageRef.put(file)
url.value = await res.ref.getDownloadURL()
}
catch(err){
console.log(err.message)
error.value = err.message
}
}
const deleteImage = async (path) => {
const storageRef = fStorage.ref(path)
try{
await storageRef.delete()
}
catch(err){
error.value = err.message
console.log(error.value)
}
}
return { error, url, filePath, uploadImage, deleteImage }
}
export default useStorage
This composable is used to add images to and remove images from firebase storage. We import getUser
so we can add images to the users image folder as well as delete the image from their image folder.
To upload an image we first need to define a filepath and save it to a ref filePath
. We then use the filePath to create a const storageRef
. Then within the try
block use storageRef
to upload the image using the firebase storage method put()
We save the response res
and then use it to get the public url to access the image and display it within the application, saving this to url
Both the filePath
and url
are also returned so can be used within the app.
deleteImage
takes in the path
as an argument when it is invoked.