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

Aaron K Saunders - Sep 10 '21 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

install vue-query

npm install vue-query
Enter fullscreen mode Exit fullscreen mode

install vue-router

npm install vue-router@next
Enter fullscreen mode Exit fullscreen mode

install firebase

npm install firebase
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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() };
};
Enter fullscreen mode Exit fullscreen mode

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
  };
Enter fullscreen mode Exit fullscreen mode

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 };
};
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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("/");
  },
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

ko-fi

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .