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 HERE2REACT_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";23const baseUrl = `https://api.cloudinary.com/v1_1/${cloudName}`;45export const makeUploadRequest = ({6 file,7 fieldName,8 progressCallback,9 successCallback,10 errorCallback,11}) => {1213 const url = `${baseUrl}/image/upload`;1415 const formData = new FormData();16 formData.append(fieldName, file);17 formData.append("upload_preset", uploadPreset);1819 const request = new XMLHttpRequest();20 request.open("POST", url);2122 request.upload.onprogress = (e) => {23 progressCallback(e.lengthComputable, e.loaded, e.total);24 };2526 request.onload = () => {27 if (request.status >= 200 && request.status < 300) {28 const { delete_token: deleteToken } = JSON.parse(request.response);2930 successCallback(deleteToken);31 } else {32 errorCallback(request.responseText);33 }34 };3536 request.send(formData);3738 return () => {39 request.abort();40 };41};4243export const makeDeleteRequest = ({44 token,45 successCallback,46 errorCallback,47}) => {4849 const url = `${baseUrl}/delete_by_token`;5051 const request = new XMLHttpRequest();52 request.open("POST", url);5354 request.setRequestHeader("Content-Type", "application/json");5556 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";23import { FilePond, registerPlugin } from "react-filepond";4import FilePondPluginImageExifOrientation from "filepond-plugin-image-exif-orientation";5import FilePondPluginImagePreview from "filepond-plugin-image-preview";67import "filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css";8import "filepond/dist/filepond.min.css";910import {11 makeDeleteRequest,12 makeUploadRequest,13} from "./cloudinary/cloudinaryHelper";1415registerPlugin(FilePondPluginImageExifOrientation, FilePondPluginImagePreview);1617function App() {18 const [files, setFiles] = useState([]);1920 const revert = (token, successCallback, errorCallback) => {21 makeDeleteRequest({22 token,23 successCallback,24 errorCallback,25 });26 };2728 const process = (29 fieldName,30 file,31 metadata,32 load,33 error,34 progress,35 abort,36 transfer,37 options38 ) => {39 const abortRequest = makeUploadRequest({40 file,41 fieldName,42 successCallback: load,43 errorCallback: error,44 progressCallback: progress,45 });4647 return {48 abort: () => {49 abortRequest();50 abort();51 },52 };53 };5455 return (56 <div style={{ width: "80%", margin: "auto", padding: "1%" }}>57 <FilePond58 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';67const root = ReactDOM.createRoot(document.getElementById('root'));8root.render(<App />);910reportWebVitals();
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: