> Before you start reading this article, make sure that your **Firebase** project is already created, you have **Blaze** or another premium plan on **Firebase**, and the **React** app is configured. We'll not dive into these concepts.
# Uploading Images with **Firebase** and **React**
Uploading images with **Firebase** and **React** is quite straightforward. Nevertheless, there are potential pitfalls that can consume your time unnecessarily. These include validation, permissions, determining the appropriate image format, and uploading logic.
With these prerequisites in mind, let's dive in! Here you have a demo of the final result, that is already working on this site!
![Demo of Image Upload](https://firebasestorage.googleapis.com/v0/b/markdown-b9f5e.appspot.com/o/AQf2hcbxgSevVmNGPhRZTJg4M7D3%2Fimages%2F354f3e69-e138-4205-ab0a-da6cfe66d5e9?alt=media)
*Demo of Image Upload*
> I simplified the example really hard, to focus on **upload** logic. If you want to check how to implement the following UI from the demo, go to the final repository that is linked at the end of the article.
## Let's Start with **Frontend**
To begin, we need to have a `file input`. Take a look at the following code:
```javascript
const FileInput = () => {
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
console.log(e.target.files); // The files list to pick up and validate.
};
return <input type="file" onChange={handleChange} />;
};
We've set up an input mechanism. Now, our next step is to ensure that any uploaded images are parsed correctly before they're sent to the Cloud Function. To accomplish this, we'll create the following parser:
The readAsDataURL enables you to read files asynchronously and obtain a data URL representation.
Now, you need to call readFileAsBase64 just after getting files from file input. Take note of the Array.isArray call, which checks whether the files have been selected by the user or not.
const handleChange: React.ChangeEventHandler<HTMLInputElement> = async (
e,
) => {
// Files may be "null" or "FileList".
if (Array.isArray(e.target.files)) {
const base64URL = await readFileAsBase64(e.target.files[0]);
}
};
Frontend is almost ready. To finish that part, we need to invoke a Cloud Function. In our case, it will be an uploadImage.
Then, in any place in the React components tree, you may import the functions variable and perform a call to Cloud Function.
import React from 'react';
import { httpsCallable } from 'firebase/functions';
import { readFileAsBase64 } from 'development-kit/file-reading';
import { functions } from './firebase-setup';
// Payload object shape.
type UploadImagePayload = {};
// Response object shape.
type UploadImageDto = {};
const FileInput = () => {
const handleChange: React.ChangeEventHandler<HTMLInputElement> = async (
e,
) => {
if (Array.isArray(e.target.files)) {
const { data } = await httpsCallable<UploadImagePayload, UploadImageDto>(
functions,
`uploadImage`,
)({ image: await readFileAsBase64(e.target.files[0]) });
}
};
return <input type="file" onChange={handleChange} />;
};
Why we've chosen the base64 representation instead of the simple FormData and File? The answer is simple, we want to have the option to upload images from clipboard without any additional headaches.
By default Cloud Functions will send a application/json type of request.
That's all on Frontend, now it's time for Backend.
Adding Storage Bucket
We need to have a place to store our images. By default, there is no storage attached to your project on Firebase. You need to create it manually, or programmatically based on your application logic. To create it manually you need to go to Firebase Console. Just follow this gif:
Finding Storage in Dashboard
In my case, I already have the storage. If it's your first time, don't worry, the UX on Firebase is great, and they will guide you. It's just several fields to populate, and nothing more.
Keep in mind, that after creating a storage, the default bucket for files will be created - this is the place where we'll save images.
Creating Rules for Storage
We need to navigate to the Storage -> Rules tab in the dashboard.
Rules Setup Location
Some changes in default rules will be required. We need to be sure that operations on our images will be allowed only for signed-in users (update, delete, write). On the other hand, we want to make images public for everyone. Of course, your app requirements may be different. It's just for the showcase:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read;
allow update, delete, write: if request.auth != null;
}
}
}
Cloud Function to Upload Image
Now, on the Backend we need to create a function called uploadImage. We'll use the onCall helper from sdk.
import { https } from 'firebase-functions';
import * as admin from 'firebase-admin';
import { v4 as uuid } from 'uuid';
admin.initializeApp();
const { onCall, HttpsError } = https;
type UploadImagePayload = { image: string };
export const uploadImage = onCall(
async ({ image }: UploadImagePayload, context) => {
const { auth } = context;
if (!auth)
throw new HttpsError(`internal`, `Only authorized users allowed`);
const storage = admin.storage();
const bucket = storage.bucket();
const [bucketExists] = await bucket.exists();
if (!bucketExists)
throw new HttpsError(
`internal`,
`No bucket. Create it manually on dashboard or use existing one`,
);
const id = uuid();
const location = `${auth.uid}/images/${id}`;
const file = bucket.file(location);
const [meta] = image.split(`,`);
const contentType = meta.split(`:`)[1].split(`;`)[0]; // image/png or other...
const extension = contentType.replace(`image/`, ``); // png or other...
const blob = image.replace(/^data:image\/\w+;base64,/, ``); // Image data.
const buffer = Buffer.from(blob, `base64`); // Buffer to upload.
await file.save(buffer, {
contentType,
});
// Unique URL to fetch image.
const url = `https://firebasestorage.googleapis.com/v0/b/${
bucket.name
}/o/${encodeURIComponent(location)}?alt=media`;
return { url, extension, contentType, id }; // It goes to Frontend
},
);
At the beginning we checked for authorization status - it may be determined by the existence of the auth property in the context object. If it's null, it means the user is not authorized.
Next, we've checked for bucket existence. Same situation, if the storage and bucket are not created, we're throwing an error.
By calling admin.storage() we're getting a reference to default storage. The same happens with the storage.bucket() call. If you want to choose another storage or bucket, you may pass a reference path as a parameter.
Later, we took the original image parameter which is the base64 format string. That's the payload from Frontend. We split it into several parts: type, extension, and data.
At that end, we've used the encodeURIComponent function. Without that, the image will not be displayed, instead, you'll get an error. It's because of the complex URL form that may contain many / characters and this encoded form contains a replaced variant.
The encodeURIComponent function transform example: This is a & test / to This%20is%20a%20%26%20test%20%2F.
Validating Image
Before uploading an image we need to have a validation mechanism, for example, how to check if the uploaded file is an image or check its size. To achieve that, at the beginning of the previous function, you can add something like this:
const buffer = Buffer.from(blob, `base64`); // Buffer to upload.
const sizeAsMegabytes = Number.parseFloat(
(Buffer.byteLength(buffer) / 1024 / 1024).toFixed(2),
);
// Checks if the image is smaller than 4 Megabytes
const hasAllowedSize = sizeAsMegabytes < 4;
If you want to check the file type, use the following code:
const extension = contentType.replace(`image/`, ``); // png or other...
const IMAGE_EXTENSIONS = [`png`, `jpeg`, `jpg`, `gif`] as const;
type ImageExtension = (typeof IMAGE_EXTENSIONS)[number];
const isFormatCorrect = IMAGE_EXTENSIONS.includes(extension as ImageExtension)
Final Result and Source Code
All code that has been shown here can be found in the following repositories:
Now you know the quick way of uploading images on Firebase Storage. We've used Cloud Functions to provide some logic on the Backend side, we performed a validation, and at the end, the image was saved on default bucket localized in storage.
On the Frontend we transformed the original File to base64 format, to prepare a base for uploading images from clipboard - especially useful on desktop devices φ(* ̄0 ̄).
After that, on mobile devices, you'll see the completely broken layout, when full preview mode is clicked.
**Definition of Done**
1. There should be an internal scroll inside the preview area, that blocks the occurrence of window x scroll.
2. Layout looks for every device from 320px+ width devices.
Context
Copy the following document content into creator:
We've set up an input mechanism. Now, our next step is to ensure that any uploaded images are parsed correctly before they're sent to the Cloud Function. To accomplish this, we'll create the following parser:
Now, you need to call
readFileAsBase64
just after getting files fromfile input
. Take note of theArray.isArray
call, which checks whether the files have been selected by the user or not.Frontend
is almost ready. To finish that part, we need to invoke a Cloud Function. In our case, it will be anuploadImage
.Then, in any place in the
React
components tree, you may import thefunctions
variable and perform a call to Cloud Function.Why we've chosen the
base64
representation instead of the simpleFormData
andFile
? The answer is simple, we want to have the option to upload images from clipboard without any additional headaches.That's all on Frontend, now it's time for Backend.
Adding Storage Bucket
We need to have a place to store our images. By default, there is no storage attached to your project on Firebase. You need to create it manually, or programmatically based on your application logic. To create it manually you need to go to Firebase Console. Just follow this gif:
Finding Storage in Dashboard
In my case, I already have the storage. If it's your first time, don't worry, the UX on Firebase is great, and they will guide you. It's just several fields to populate, and nothing more.
Creating Rules for Storage
We need to navigate to the
Storage
->Rules
tab in the dashboard.Rules Setup Location
Some changes in default rules will be required. We need to be sure that operations on our images will be allowed only for signed-in users (update, delete, write). On the other hand, we want to make images public for everyone. Of course, your app requirements may be different. It's just for the showcase:
Cloud Function to Upload Image
Now, on the Backend we need to create a
function
calleduploadImage
. We'll use theonCall
helper fromsdk
.At the beginning we checked for authorization status - it may be determined by the existence of the
auth
property in thecontext
object. If it'snull
, it means the user is not authorized.Next, we've checked for
bucket
existence. Same situation, if thestorage
andbucket
are not created, we're throwing an error.Later, we took the original
image
parameter which is thebase64 format string
. That's the payload fromFrontend
. We split it into several parts: type, extension, and data.At that end, we've used the
encodeURIComponent
function. Without that, the image will not be displayed, instead, you'll get an error. It's because of the complexURL
form that may contain many/
characters and this encoded form contains a replaced variant.Validating Image
Before uploading an image we need to have a validation mechanism, for example, how to check if the uploaded file is an image or check its size. To achieve that, at the beginning of the previous function, you can add something like this:
If you want to check the file type, use the following code:
Final Result and Source Code
All code that has been shown here can be found in the following repositories:
Summary
Now you know the quick way of uploading images on
Firebase Storage
. We've used Cloud Functions to provide some logic on the Backend side, we performed a validation, and at the end, the image was saved on default bucket localized in storage.On the Frontend we transformed the original File to
base64
format, to prepare a base for uploading images fromclipboard
- especially useful on desktop devices φ(* ̄0 ̄).