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>