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
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:
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:
const readFileAsBase64 = (file: File): Promise<FileReader['result']> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
// Converts image to URL -> data:image/jpeg;base64,
reader.readAsDataURL(file);
});
};
export { readFileAsBase64 };
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 a uploadImage
.
import { FirebaseOptions, initializeApp } from 'firebase/app';
import { getFunctions } from 'firebase/functions';
// firebase-setup.ts
const config: FirebaseOptions = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
projectId: process.env.FIREBASE_PROJECT_ID,
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.FIREBASE_APP_ID,
measurementId: process.env.FIREBASE_MEASURMENT_ID,
};
const app = initializeApp(config);
const functions = getFunctions(app);
export { app, functions };
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 = {
image: string;
};
// Response object shape.
type UploadImageResponse = {};
const FileInput = () => {
const handleChange: React.ChangeEventHandler<HTMLInputElement> = async (
e,
) => {
if (Array.isArray(e.target.files)) {
const { data } = await httpsCallable<UploadImagePayload, UploadImageResponse>(
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 aapplication/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 defaultstorage
. The same happens with thestorage.bucket()
call. If you want to choose anotherstorage
orbucket
, 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)
Changing Images Quality and Size
Some storage providers, such as Google Cloud, have specific costs per GB or offer a type of free tier. If you store original images uploaded by users, you will quickly exhaust your budget. For instance, only 500 images, each 10 MB, are needed to exceed the free tier limit on Google. This happens quite rapidly. Afterward, you'll incur a cost of $0.28 per GB. If your user base grows significantly, the expenses could become exorbitant. That's why it's advisable to reduce the quality and size of images during upload.
Don't worry, there's a cool library that allows you to do this with just a few function calls. It should be installed in your backend codebase. To install the sharp
library, type npm i sharp --save
in your terminal.
Let's say we want to convert every input image (png, jpeg, jpg)
to the modern webp
format and reduce its size and quality (I've omitted format validation, as it's done beforehand).
import * as sharp from 'sharp';
const buffer = Buffer.from(blob, `base64`);
const webpBuffer = await sharp(buffer)
.resize(width, height)
.webp({ quality: 60 })
.toBuffer();
// Decreased quality image saved in storage.
await file.save(webpBuffer, {
contentType,
});
const url = `https://firebasestorage.googleapis.com/v0/b/${
bucket.name
}/o/${encodeURIComponent(path)}?alt=media`;
return { url };
This library has other useful methods for reading image metadata. Additionally, you can change the aspect ratio, apply effects, or transform images. Feel free to explore it! Here you've Sharp examples.
Final Result and Source Code
All code that has been shown here can be found in the following repositories:
- Backend - https://github.com/polubis/greenonsoftware-api/tree/develop/functions/src.
- Frontend - https://github.com/polubis/4markdown/tree/develop.
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 from clipboard
- especially useful on desktop devices φ(* ̄0 ̄).