Using Vue Query Hooks For Firebase Database and Image Storage, Mutations and Queries in VueJS

Using Vue Query Hooks For Firebase Database and Image Storage, Mutations and Queries in VueJS

Vue Query package provides hooks for fetching, caching and updating asynchronous data in Vue. Based on react-query. All main concepts are inherited from the main package. Please also check the react query docs*

I love React Query so I thought it would be great to checkout Vue Query and put together a quick code walkthrough of a simple project. I used Firebase because I already had a project in place, but it could have been any database provider, Supabase might be next !!

Video

High Level Walkthrough of the video including the source code used

Create Vue Project using Vite

npm init vite@latest

install vue-query

npm install vue-query

install vue-router

npm install vue-router@next

install firebase

npm install firebase

Create .env file containing firebase credentials in the root of your project

VITE_APP_PROJECT_ID=image-bah
VITE_APP_PROJECT_BUCKET=image-bah.appspot.com

Firebase Functions - we are using the new version of firebase javascript SDK in this example so things look a bit different.

import {
  addDoc,
  collection,
  doc,
  getDoc,
  getDocs,
  getFirestore,
  setDoc,
} from "firebase/firestore";
import { getDownloadURL, getStorage, ref, uploadBytes } from "firebase/storage";

Get All Items

/**
 * the function called with the vue-query useQuery hook when the page
 * is rendered
 */
export const getAllImages = async (): Promise<any[]> => {
  const results: any[] = [];
  const snap = await getDocs(collection(getFirestore(), "ImageInfo"));
  snap.forEach((doc) => {
    results.push({ id: doc.id, ...doc.data() });
  });
  return results;
};

Get A Single Item

/**
 * function to query firebase for the specified document
 */
export const getImageDocument = async (docId) => {
  const snap = await getDoc(doc(getFirestore(), "ImageInfo", docId));
  if (!snap.exists()) throw `Document ${docId} Not Found`;
  return { id: snap.id, ...snap.data() };
};

Delete A Single Item

/**
 * function to delete a specified document from firebase  
 */
 export const deleteImageDocument = async (docId: string): Promise<any> => {
    const snap = await deleteDoc(doc(getFirestore(), "ImageInfo", docId));
    return true
  };

Upload Image Information

/**
 * upload image tp storage and save additional information in
 * imageData collection
 */
export const uploadImageInfo = async (params: File) => {
  console.log(params);

  const storageRef = ref(getStorage(), `images/${params.name}`);
  // 'file' comes from the Blob or File API
  const snapshot = await uploadBytes(storageRef, params, {
    contentType: params.type,
  });
  console.log("Uploaded a blob or file!", snapshot);

  const url = await getDownloadURL(storageRef);

  await addDoc(collection(getFirestore(), "ImageInfo"), {
    imageData: {
      size: snapshot.metadata.size,
      contentType: snapshot.metadata.contentType,
    },
    name: snapshot.metadata.name,
    url,
  });

  return { data: snapshot };
};

Set up routes and initialize firebase

import { createApp } from "vue";
import Home from "./Home.vue";
import Detail from "./Detail.vue";
import App from "./App.vue";

import { createRouter, createWebHistory } from "vue-router";
import { initializeApp } from "firebase/app";

const routes = [
  {
    path: "/",
    name: "Home",
    component: Home,
  },
  {
    path: "/detail/:docId",
    name: "Detail",
    component: Detail,
    props: true,
  },
];
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: routes as any,
});

// initialize firebase
const app = initializeApp({
  projectId: import.meta.env.VITE_APP_PROJECT_ID as string,
  storageBucket: import.meta.env.VITE_APP_PROJECT_BUCKET as string,
});

createApp(App).use(router).mount("#app");

Listing All Items In Collection

The home component is just the list component showing the data from the firebase storage collection

<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import ImageList from './components/ImageList.vue'

</script>

<template>
  <ImageList  />
</template>

The ImageList Component, first lets just get a list of date using vue-query, We have imported from firebase-functions the call to query the database getAllImages

<script setup lang="ts">
import { useQuery, useQueryClient } from "vue-query";
import { getAllImages } from "../firebase-functions";

//A QUERY CLIENT
const queryClient = useQueryClient();

// A QUERY HOOK
const { isLoading, isError, isFetching, data, error, refetch } = useQuery(
  "images",
  getAllImages
);
</script>

<template>
  <div v-if="isLoading">Loading...</div>
  <div v-else-if="isError">An error has occurred: {{ error }}</div>

</template>

<style scoped>
/* style removed for brevity */
</style>

Getting A Single Item From the Collection

We have created a detail page to demonstrate how to query just one item from the collection. This script import the firebase function getImageDocument. The document id is passed a parameter and it is used in the useQuery hook to query the database for the document

<script setup lang="ts">
import { useQuery } from "vue-query";
import { getImageDocument } from "./firebase-functions";

//* Define Properties used in Component
const { docId } = defineProps<{ docId: string }>();

// query hook for one item, based on the docId
const { isLoading, isError, isFetching, data, error, refetch } = useQuery(
  ["images", docId],
  // call query with parameter
  () => getImageDocument(docId as any)
);
</script>

<template>
  <section>
    <button @click="$router.replace('/')" style="margin: 16px">GO HOME</button>
 <div v-if="isLoading">Loading...</div>
    <div v-else-if="isError">An error has occurred: {{ error }}</div>
    <div v-else-if="data">
      <div style="width: 100%">
       <img :src="'data.url'"
          style="
            display: block;
            margin-left: auto;
            margin-right: auto;
            width: 50%;
          "
        />
      </div>
      <div style="margin: 16px; overflow-wrap: break-word">
        <div>{{ data.name }}</div>
        <div>{{ data.imageData.size }}</div>
        <div>{{ data.imageData.contentType }}</div>
      </div>
    </div>
  </section>
</template>

Adding An Item To A Collection, or Mutations

We have added a new section to the template in Home.vue where we have a button that displays the input to select a file and we will upload the file to firebase storage and save some information in a collection.

<section>
  <!-- if we get a mutation error, display it -->
  <div v-if="mutation.isError.value === true">
    An error has occurred: {{ mutation?.error.value }}
  </div>

  <!-- input element to capture new file -->
  <input
    id="file-upload"
    type="file"
    style="display: none"
    @change="
      (e) => {
        e?.target?.files?.length && mutation.mutate(e?.target?.files[0]);
      }
    "
  />
  <div>
    <button @click="openFileDialog">Upload New Image</button>
  </div>
</section>

In the script section we have added a few new functions and introduced the useMutation hook. The mutation object returned has a mutate function that we call to actually upload the file.

// A MUTATION HOOK, call the mutation function and on success
// clear all of the images so that they are reloaded with the new
// data
const mutation = useMutation(uploadImageInfo, {
  onSuccess: () => {
    queryClient.invalidateQueries("images");
  },
});

/**
 * opens the file dialog when the button is clicked
 */
const openFileDialog = () => {
  document?.getElementById("file-upload")?.click();
};

Deleting An Item

in the Detail component we have a button that will trigger another mutation to delete the document using the firebase function we covered earlier. The delete mutation looks like this

// A MUTATION HOOK
const mutation = useMutation(deleteImageDocument, {
  onSuccess: () => {
    debugger;
    queryClient.invalidateQueries("images");
    router.replace("/");
  },
});

we also make changes to the template, one is to catch any mutation errors

<div v-if="mutation.isError.value === true">
  An error has occurred: {{ mutation?.error.value }}
</div>

the other is the addition of the button to trigger the delete

<div style="display: flex; justify-content: flex-end">
  <button @click="mutation.mutate(docId)" style="margin: 16px">
    DELETE
  </button>
</div>