Using Vue Query Hooks For Firebase Database and Image Storage, Mutations and Queries in VueJS
Tags: vite, vue, javascript, vuequery
Published: 2021-09-10 14:52:26

> **[Vue Query](https://github.com/DamianOsipiuk/vue-query)** package provides hooks for fetching, caching and updating asynchronous data in Vue. Based on **[react-query](https://github.com/tannerlinsley/react-query)**. All main concepts are inherited from the main package. Please also check the **[react query docs](https://react-query.tanstack.com/overview)***

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
{% youtube m_K4scUBGyE %}

**High Level Walkthrough of the video including the source code used**

Create Vue Project using Vite

```json
npm init vite@latest
```

install vue-query

```json
npm install vue-query
```

install vue-router

```json
npm install vue-router@next
```

install firebase

```json
npm install firebase
```

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

```json
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.

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

Get All Items

```jsx
/**
 * 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

```jsx
/**
 * 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

```jsx
/**
 * 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

```jsx
/**
 * 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

```jsx
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

```jsx
<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`

```jsx
<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

```jsx
<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.

```jsx
<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.

```jsx
// 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

```jsx
// 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

```jsx
<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

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

[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/W7W31U7HM)