Upload Files to Firebase Cloud Storage using Vue 3, Typescript, and the Composition API

Amenallah Hsoumi
RunThatLine
Published in
5 min readNov 17, 2022

In this tutorial, we will build a useFileUpload Vue 3 Composable which uploads files to Firebase Cloud Storage. We will also be using Typescript and I assume that you have a working Vue project with Firebase setup.

If that's not the case then you can check out the first part of this tutorial. I will also be explaining a few things using comments inside the code.

Our composable will be basically an encapsulation of reusable stateful logic and a single method, all related to one feature, that is file upload.

We will first create the initial state, then a file upload method. Let's get started!

The Initial State

For our local state, we store the download URL, which we can fetch after uploading successfully, the upload progress, state, and whether it failed or not.

// composables/useFileUpload/useFileUpload.composable.ts

import { type TaskState } from 'firebase/storage'
import { ref } from 'vue'

const useFileUpload = () => {
const downloadUrl = ref('')
const hasFailed = ref(false)
const state = ref<TaskState>()
const progress = ref(0)

return {
downloadUrl,
hasError,
state,
progress,
}
}

export default useFileUpload

Notice that we import type TaskStateand not just TaskState. I have learned recently, that this signals Typescript not to include the interface in the Javascript bundle, since in this context it's used for typing purposes only.

Next, we provide another feature for the consumer, the progress in percentage.

// update the imports
import { ref, computed } from 'vue'

// ... other code

const progressInPercentage = computed(() => {
return `${progress.value}%`
})

return {
downloadUrl,
hasFailed,
state,
progress,
progressInPercentage,
}

The Upload Method

To upload a file we use the magical uploadFileResumable function from firebase/storage. It returns an UploadTask which we can use to get the upload percentage, pause, resume, or even cancel uploading.

First, update the imports from firebase/storage.

import {
type FirebaseStorage,
type TaskState,
getDownloadURL,
ref as firebaseStorageRef, // to avoid conflict with `ref` import from vue
uploadBytesResumable,
} from 'firebase/storage'

Next, inside our composable method, we create uploadFile.

/**
* Uploads a file to firebase cloud storage.
*
* @param param.storage Firebase storage instance
* @param param.path Upload path, example: "images/image.png"
* @param param.data Data to upload
*
*/
const uploadFile = ({
storage,
path,
data,
}: {
storage: FirebaseStorage
path: string
data: Blob | Uint8Array | ArrayBuffer
}) => {
// reset every time we upload
hasFailed.value = false
progress.value = 0

const storageRef = firebaseStorageRef(storage, path)
const uploadTask = uploadBytesResumable(storageRef, data)

uploadTask.on(
'state_changed',
(snapshot) => {
progress.value = calculatePercentage(
snapshot.bytesTransferred,
snapshot.totalBytes,
)
state.value = snapshot.state
},
() => {
hasFailed.value = true
},
async () => {
// upload has completed successfully -> update state & generate download link.
state.value = 'success'
downloadUrl.value = await getDownloadURL(uploadTask.snapshot.ref)
},
)

return uploadTask
}

return {
downloadUrl,
hasFailed,
state,
progress,
progressInPercentage,
uploadFile,
}

The function accepts the firebase storage instance, the data to upload which can be one of three types, and the path to upload to.

The first callback of the upload task state_changed listener accepts the snapshot of type UploadTaskSnapshot and uses the bytesTransferred and totalBytes properties to calculate the upload percentage.

Furthermore, we store the upload state, I will showcase how we use this later.

We need to create the calculatePercenage function outside of the composition method.

/**
* @returns Rounded number [0..100]
*/
const calculatePercentage = (
numerator: number,
denominator: number,
): number => {
// do not divide by zero 😨
if (denominator === 0) {
return 0
}

return Math.round((numerator / denominator) * 100)
}

This is a utility function. Since the only usage is in this file we can keep it here. We can move it later to a util file when it's used somewhere else.

Moving on, the second callback will fire when an error occurs. We flip the hasFailed boolean to true. We use this later to display a nice message to the user.

The third and final callback we provide will be invoked when the upload has been successful, at this point, we can generate a download URL and set the status to success.

Using the Composable

Let’s now put this to use, inside a component we import and call the composable, then unpack the state and method into variables.

<script setup lang="ts">
import { useFileUpload } from '../lib'

const {
uploadFile,
hasFailed,
state,
downloadUrl,
progressInPercentage,
} = useFileUpload()

</script>

We also need a local ref here to hold the input file to upload

import { ref } from 'vue'

// ...code

const file = ref<File>()

Next, we create the template.

<template>
<input type="file" @change="onInputChange" />

<br /><br />

<button
:class="$style.button"
@click="onUploadClick"
:disabled="!file || state === 'running'"
>
<template v-if="state === 'running'">
Uploading: {{ progressInPercentage }}
</template>

<template v-else> Upload! </template>
</button>

<br /><br />

<p v-if="hasFailed">Upload Failed!</p>

<p v-if="!!downloadUrl">
{{ downloadUrl }}
</p>
</template>

When the input emits a change event, we call onInputChange where we set the file ref to equal the file inside the input.

the button will be disabled until a file is selected, after that we can click on it, and it will call onUploadClick .

We display the upload percentage while uploading and an error message if it failed, otherwise the download URL after it finishes.

The methods we call inside the template should look like this.

const onUploadClick = () => {
if (file.value) {
uploadFile({
storage,
path: file.value.name,
data: file.value as Blob,
})
}
}

const onInputChange = (event: Event) => {
const { files } = event.target as HTMLInputElement

if (files) {
file.value = files[0]
}
}

Storage will be a FirebaseStorage that we get after invoking thegetStorage function.

import { getStorage } from '@firebase/storage'

// app is the firebase app
const storage = getStorage(app, '<insert_bucket_url_here')

Finally, we can add a wee bit of CSS to make it look a bit nicer.

<style module>
.button {
border: none;
background: none;
cursor: pointer;
padding: 8px 12px;
background-color: #119ada;
color: #fff;
}

.button:disabled,
.button[disabled] {
border: 1px solid #999999;
background-color: #cccccc;
color: #666666;
cursor: not-allowed;
}
</style>

Time for a test drive 😎

😲

--

--