Upload Multiple Images to Cloudinary with FilePond

Ifeoma Imoh

When building sites, the file upload functionality tends to be more complex than anticipated. It’s no longer enough to have an input form that sends documents to some server. These days one has to consider asynchronous file uploads, progress bars, etc. Additionally, it must be abstracted and covered with a visually appealing form that users will find intuitive. This is where FilePond has found its relevance.

Filepond provides a visually appealing file upload interface that allows you to drag and drop files. It also automatically handles the upload process and progress updates. Additionally, it provides a feature to revert uploads — all of which we will be exploring in this article.

We will build a React application that uses Filepond to upload multiple images to a Cloudinary store and also (should the user decide to) delete the images.

Here is a link to the demo on CodeSandbox.

Setting up the Project

Create a new React application by running the following command:

1npx create-react-app cloudinary_filepond

Next, add FilePond to the React application by running the following command:

1npm install react-filepond filepond

We’ll also be using two FilePond plugins - the Image exif-orientation plugin, which is used by the Image preview plugin to ensure that the preview of the uploaded image is displayed correctly. Add both plugins using the following command:

1npm install filepond-plugin-image-preview filepond-plugin-image-exif-orientation

For this tutorial, we will be sending images to Cloudinary via unsigned POST requests, but you need to first sign up for a free Cloudinary account if you don't have one already. Displayed on your account's Management Console (aka Dashboard) are important details: your cloud name, API key, etc. We will need our account cloud name and an unsigned upload preset. To create one, log into the Management Console and select Settings > Upload and then scroll to the Upload presets section. Create a new upload preset by clicking Add upload preset at the bottom of the upload preset list. In the displayed form, make sure the Signing Mode is set to Unsigned as shown below.

Next, in the Upload Control section, ensure that the Return delete token option is turned on. This will allow us to delete uploaded images within 10 minutes of uploading to Cloudinary.

Click Save to complete the upload preset definition, then copy the upload preset name as displayed on the Upload Settings page.

Next, we need to create some environment variables in our React application to hold our Cloudinary details. Create a new file at the root of your project called .env and add the following to it:

1REACT_APP_CLOUD_NAME = INSERT YOUR CLOUD NAME HERE
2REACT_APP_UPLOAD_PRESET = INSERT YOUR UNSIGNED UPLOAD PRESET KEY HERE

This will be used as a default when the project is set up on another system. To update your local environment, create a copy of the .env file using the following command:

1cp .env .env.local

By default, this local file is added to .gitignore and mitigates the security risk of inadvertently exposing secret credentials to the public. You can update the file with your Cloudinary cloud name and generated upload preset.

In the src directory of the project, create a new folder named cloudinary. This folder will hold all the Cloudinary-related helper classes we will need in our components. In the cloudinary folder, create a new file called cloudinaryConfig.js. This file will give access to the environment variables and prevent repeated process.env. calls throughout the project. In cloudinaryConfig.js, add the following:

1export const cloudName = process.env.REACT_APP_CLOUD_NAME;
2export const uploadPreset = process.env.REACT_APP_UPLOAD_PRESET;

Create Helper Class for API Requests

Let’s write a helper function that we will use to upload images to Cloudinary and another to delete images from Cloudinary. In the cloudinary folder, create a new file named cloudinaryHelper.js and add the following code to it:

1import { cloudName, uploadPreset } from "./cloudinaryConfig";
2
3const baseUrl = `https://api.cloudinary.com/v1_1/${cloudName}`;
4
5export const makeUploadRequest = ({
6 file,
7 fieldName,
8 progressCallback,
9 successCallback,
10 errorCallback,
11}) => {
12
13 const url = `${baseUrl}/image/upload`;
14
15 const formData = new FormData();
16 formData.append(fieldName, file);
17 formData.append("upload_preset", uploadPreset);
18
19 const request = new XMLHttpRequest();
20 request.open("POST", url);
21
22 request.upload.onprogress = (e) => {
23 progressCallback(e.lengthComputable, e.loaded, e.total);
24 };
25
26 request.onload = () => {
27 if (request.status >= 200 && request.status < 300) {
28 const { delete_token: deleteToken } = JSON.parse(request.response);
29
30 successCallback(deleteToken);
31 } else {
32 errorCallback(request.responseText);
33 }
34 };
35
36 request.send(formData);
37
38 return () => {
39 request.abort();
40 };
41};
42
43export const makeDeleteRequest = ({
44 token,
45 successCallback,
46 errorCallback,
47}) => {
48
49 const url = `${baseUrl}/delete_by_token`;
50
51 const request = new XMLHttpRequest();
52 request.open("POST", url);
53
54 request.setRequestHeader("Content-Type", "application/json");
55
56 request.onload = () => {
57 if (request.status >= 200 && request.status < 300) {
58 successCallback();
59 } else {
60 errorCallback(request.responseText);
61 }
62 };
63 request.send(JSON.stringify({ token }));
64};

Requests are sent to Cloudinary using XMLHttpRequest Objects. In the makeUploadRequest function, we provide a function named progressCallback, which is used to update the progress indicator of the Filepond UI. Additionally, a function is provided, which is executed when a successful response is received — successCallback. This function takes the delete token provided by Cloudinary in the response. The errorCallback function is executed if an error response is returned.

The makeUploadRequest function returns a function that can be called if the user chooses to cancel the upload before it is completed.

In a similar vein, the makeDeleteRequest takes a token, a successCallback function, and an errorCallback function which are executed upon receipt of successful and error responses, respectively.

Next, update your src/App.js file to match the following:

1import React, { useState } from "react";
2
3import { FilePond, registerPlugin } from "react-filepond";
4import FilePondPluginImageExifOrientation from "filepond-plugin-image-exif-orientation";
5import FilePondPluginImagePreview from "filepond-plugin-image-preview";
6
7import "filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css";
8import "filepond/dist/filepond.min.css";
9
10import {
11 makeDeleteRequest,
12 makeUploadRequest,
13} from "./cloudinary/cloudinaryHelper";
14
15registerPlugin(FilePondPluginImageExifOrientation, FilePondPluginImagePreview);
16
17function App() {
18 const [files, setFiles] = useState([]);
19
20 const revert = (token, successCallback, errorCallback) => {
21 makeDeleteRequest({
22 token,
23 successCallback,
24 errorCallback,
25 });
26 };
27
28 const process = (
29 fieldName,
30 file,
31 metadata,
32 load,
33 error,
34 progress,
35 abort,
36 transfer,
37 options
38 ) => {
39 const abortRequest = makeUploadRequest({
40 file,
41 fieldName,
42 successCallback: load,
43 errorCallback: error,
44 progressCallback: progress,
45 });
46
47 return {
48 abort: () => {
49 abortRequest();
50 abort();
51 },
52 };
53 };
54
55 return (
56 <div style={{ width: "80%", margin: "auto", padding: "1%" }}>
57 <FilePond
58 files={files}
59 acceptedFileTypes="image/*"
60 onupdatefiles={setFiles}
61 allowMultiple={true}
62 server={{ process, revert }}
63 name="file"
64 labelIdle='Drag & Drop your files or <span class="filepond--label-action">Browse</span>'
65 />
66 </div>
67 );
68}
69export default App;

The App component has one state variable — files, which is used to keep track of the images selected by the user. Next, we declared two functions: process and revert, which handle the upload and delete operations. These functions are passed as props to the Filepond component.

In the process function, Filepond makes nine parameters available - however, we only need six for our use case. The fieldName and file parameters are appended to the FormData request, sent to Cloudinary. The load function is called upon successful execution of the request, and it takes a string - in our case, the delete token for the uploaded image, which is used to identify each file uniquely. When the revert function is called, Filepond knows exactly which image to delete. The progress function is used to update the progress bar, while the error function takes a string that displays as an error message. Just as we did for the makeUploadRequest function, the process function returns a function that is used to abort the upload process.

With this in place, our application is ready to test. Start the application using the following command:

1npm start

Disabling React Strict Mode

When you run your application, you may see an error message in the browser console similar to the one shown below:

1Uncaught TypeError: Cannot read properties of null (reading 'insertBefore')

Disable React strict mode in the src/Index.js to get rid of the error. To do this, open the src/Index.js and update it to match the following:

1import React from 'react';
2import ReactDOM from 'react-dom/client';
3import './index.css';
4import App from './App';
5import reportWebVitals from './reportWebVitals';
6
7const root = ReactDOM.createRoot(document.getElementById('root'));
8root.render(<App />);
9
10reportWebVitals();

Find the complete project here on GitHub.

Conclusion

In this article, we looked at how we can combine Filepond and Cloudinary to simplify the process of uploading multiple files while providing an intuitive interface with the ability to revert uploads - even after completion. We achieved a balance between complex functionality and code maintainability by leveraging them.

Resources you may find helpful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.