Upload Images Using Drag and Drop API (2/2)

Ifeoma Imoh

It’s one thing to create a beautiful drag and drop feature that allows you to select multiple images at once and quite another to upload multiple files to Cloudinary. Well, who says you can’t do both?

This is the second of a two-part series. In this article, I will show you how to upload multiple files to Cloudinary using a drag and drop API. In addition to that, we will render the uploaded images in a separate view. The application will have an index view displaying the drag and drop form, allowing you to select multiple images at once. When the submit button is clicked, the files will be uploaded, and on completion, the application will render the uploaded images in a grid. React Router will be used to handle the transition between views.

We will take advantage of Cloudinary's comprehensive API and easy-to-use SDKs to manage our media resources. You will need a Cloudinary account for this tutorial. Create one here if you don't already have one.

For UI components in our application, we will use antd, while axios will be used for uploading our images to our Cloudinary store and rendering the resulting images.

Here is a link to the demo on CodeSandbox.

Project Setup

If you followed the first part of this series, you can skip this step and move on to the next step. Clone this repository and install the packages using the following commands:

1git clone https://github.com/ifeoma-imoh/drag-and-drop-demo
2
3 cd drag-and-drop-demo
4
5 npm install

Once this is done, we can install the dependencies we’ll need for our application.

1npm i axios react-router-dom@6 cloudinary-react

Next, we need to create some environment variables to hold our Cloudinary details. We will be sending images to Cloudinary via unsigned POST requests for this tutorial. To do this, we need our account cloud name and an unsigned upload preset. Create a new file called .env at the root of the project and add the following to it:

1REACT_APP_CLOUD_NAME = 'INSERT YOUR CLOUD NAME HERE'
2 REACT_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 this 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 .env.local with your Cloudinary cloud name and generated upload preset.

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

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

Create Helper Class for External Requests

We need to make two requests for this application - one to upload a new image to Cloudinary and the other to download the image from a URL our application will build. In the utility folder, create a new file named api.js and add the following to it:

1import axios from 'axios';
2 import { cloudName, uploadPreset } from './cloudinaryConfig';
3 export const uploadImage = ({ file, successCallback }) => {
4 const url = `https://api.cloudinary.com/v1_1/${cloudName}/image/upload`;
5 const data = new FormData();
6 data.append('file', file);
7 data.append('upload_preset', uploadPreset);
8 axios
9 .post(url, data, {
10 headers: {
11 'Content-Type': 'multipart/form-data',
12 },
13 })
14 .then((response) => successCallback(response.data));
15 };

The uploadImage function is used to upload the provided file to Cloudinary. It makes a POST request to Cloudinary, providing the file and our earlier defined upload preset. If the request is handled successfully, the provided callback is executed, passing the Cloudinary response as a function parameter.

Implement Upload Functionality

Let’s implement the function to upload files to Cloudinary. At the moment, we have a hook that provides the functionality needed for file selection. We can add another function to submit the files and add it to the returned array. Open the src/hooks/useFileSelection.js file and update its content as shown below:

1import { message } from 'antd';
2 import { useState } from 'react';
3 import { uploadImage } from '../utility/api';
4 import { useNavigate } from 'react-router-dom';
5 const useFileSelection = () => {
6 const [selectedFiles, setSelectedFiles] = useState([]);
7 const [isUploading, setIsUploading] = useState(false);
8 const navigate = useNavigate();
9 const addFile = (file) => {
10 setSelectedFiles((currentSelection) => [...currentSelection, file]);
11 };
12 const removeFile = (file) => {
13 setSelectedFiles((currentSelection) => {
14 const newSelection = currentSelection.slice();
15 const fileIndex = currentSelection.indexOf(file);
16 newSelection.splice(fileIndex, 1);
17 return newSelection;
18 });
19 };
20 const uploadSelection = () => {
21 if (selectedFiles.length === 0) {
22 message.error('You need to select at least one image');
23 return;
24 }
25 setIsUploading(true);
26 const uploadResults = [];
27 selectedFiles.forEach((file) => {
28 uploadImage({
29 file,
30 successCallback: (response) => {
31 uploadResults.push(response);
32 if (uploadResults.length === selectedFiles.length) {
33 setIsUploading(false);
34 message.success('Images uploaded successfully');
35 navigate('/results', { state: uploadResults });
36 }
37 },
38 });
39 });
40 };
41 return [addFile, removeFile, isUploading, uploadSelection];
42 };
43 export default useFileSelection;

We’ve made a few additions. First, we added a boolean state variable to indicate when the images are being uploaded, and we can use this to modify the UI to keep the user informed.

Next, we added a function named uploadSelection. When more than one image is selected, the function iterates through each image and makes a POST request to the Cloudinary store using the uploadImage function we declared earlier. If the upload is successful, we append the response data to an array named uploadResults. By comparing the lengths of the selectedFiles and uploadResults array, we can determine when the upload is complete and redirect to the /results route (which we will configure later). To keep things simple, we pass the results via the navigate function, which we retrieved from React router’s useNavigate hook.

Finally, we updated the returned array to include the uploadSelection function and the isUploading variable.

Build ImageUpload Component

At the moment, our src/App.js file contains the JSX and functionality for selecting/uploading images. Before we add anything else, let’s move everything to a new component to keep things lean and simple. In the src/components directory, create a new file named ImageUpload.js and add the following to it:

1import useFileSelection from '../hooks/useFileSelection';
2 import { Button, Card } from 'antd';
3 import DragAndDrop from './DragAndDrop';
4 const ImageUpload = () => {
5 const [addFile, removeFile, isUploading, uploadSelection] =
6 useFileSelection();
7 return (
8 <Card
9 style={{ margin: 'auto', width: '50%' }}
10 actions={[
11 <Button type="primary" loading={isUploading} onClick={uploadSelection}>
12 Submit
13 </Button>,
14 ]}
15 >
16 <DragAndDrop addFile={addFile} removeFile={removeFile} />
17 </Card>
18 );
19 };
20 export default ImageUpload;

In addition to moving the JSX and functionality to a new component, we’ve also integrated the upload functionality by including isUploading and uploadSelection in our destructuring of the useFileSelection hook.

We’ve also added an onClick handler to the submit button, which calls the uploadSelection function. While the upload is in progress, the button state is set to loading via the loading prop.

Build Image Component

Next, we need a component to render a single image in the results route. In the src/components directory, create a new file called Image.js and add the following to it:

1import { Card } from 'antd';
2 import { Image as CloudinaryImage, Transformation } from 'cloudinary-react';
3 import { cloudName, uploadPreset } from '../utility/cloudinaryConfig';
4 const { Meta } = Card;
5 const Image = ({ publicId, originalFilename, createdAt }) => {
6 const dateString = new Intl.DateTimeFormat('en-GB', {
7 dateStyle: 'full',
8 timeStyle: 'long',
9 }).format(new Date(createdAt));
10 return (
11 <Card
12 hoverable
13 style={{ width: 240 }}
14 cover={
15 <CloudinaryImage
16 publicId={`${publicId}.png`}
17 cloudName={cloudName}
18 upload_preset={uploadPreset}
19 secure={true}
20 alt={originalFilename}
21 >
22 <Transformation width={240} height={300} crop="scale" />
23 </CloudinaryImage>
24 }
25 >
26 <Meta
27 title={originalFilename}
28 description={`Uploaded on ${dateString}`}
29 />
30 </Card>
31 );
32 };
33 export default Image;

This component takes the public id of the image, the original filename, and the Date Time at which it was uploaded as props and renders the image using the Cloudinary-react SDK. It performs one transformation on the image - scaling it to a height of 240px and width of 300px. The image is wrapped in a Card containing the original filename as its title and the upload Date Time as the description.

Build ImageGrid Component

Next, let’s add a component that will be rendered for the results route. This component will take the uploaded results passed and render each result using the Image component we declared earlier. In the src/components directory, create a new file called ImageGrid.js and add the following to it:

1import { Row, Col, PageHeader } from 'antd';
2 import { useEffect, useState } from 'react';
3 import { useLocation, useNavigate } from 'react-router-dom';
4 import Image from './Image';
5 const ImageGrid = () => {
6 const location = useLocation();
7 const navigate = useNavigate();
8 const [resources, setResources] = useState([]);
9 useEffect(() => {
10 const uploadResults = location.state;
11 setResources(uploadResults);
12 }, []);
13 const returnToUploads = () => {
14 navigate('/');
15 };
16 return (
17 <div style={{ margin: '5%' }}>
18 <PageHeader
19 ghost={true}
20 onBack={returnToUploads}
21 title="Upload Pictures"
22 />
23 <Row gutter={[0, 16]} align="middle">
24 {resources.map((image) => {
25 const { original_filename, public_id, created_at } = image;
26 return (
27 <Col span={6}>
28 <Image
29 key={public_id}
30 publicId={public_id}
31 originalFilename={original_filename}
32 createdAt={created_at}
33 />
34 </Col>
35 );
36 })}
37 </Row>
38 </div>
39 );
40 };
41 export default ImageGrid;

In this component, we make a useEffect call which runs once when the component is mounted. This call retrieves the uploaded results passed to the component via the location state (provided by react-router-dom) and updates the resources state variable accordingly. Next, we declare a function named returnToUploads which takes the user back to the index page (where we upload images).

Finally, we return the JSX for this component. Before iterating through the results and rendering the associated images, we add a PageHeader component, which is used to return to the image uploads view.

Routing Implementation

Now that we have all our components in place, let’s implement the routing functionality to navigate between components. In the src/utility folder, create a new file called routes.js and add the following to it:

1import ImageUpload from '../components/ImageUpload';
2 import ImageGrid from '../components/ImageGrid';
3 const routes = [
4 {
5 path: '/',
6 element: <ImageUpload />,
7 },
8 {
9 path: '/results',
10 element: <ImageGrid />,
11 },
12 ];
13 export default routes;

Here we define Route Objects used by the useRoutes hook to render the <Route> component.

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

1import './App.css';
2 import { useRoutes } from 'react-router-dom';
3 import routes from './utility/routes';
4 const App = () => {
5 const router = useRoutes(routes);
6 return <div style={{ margin: '1%' }}>{router}</div>;
7 };
8 export default App;

Using the earlier declared routes object and the useRoutes hook, we render a <Route> component.

Finally, wrap the <App> component in a BrowserRouter. To do this, update src/index.js to match the following:

1import React from 'react';
2 import ReactDOM from 'react-dom';
3 import './index.css';
4 import App from './App';
5 import reportWebVitals from './reportWebVitals';
6 import { BrowserRouter } from 'react-router-dom';
7 ReactDOM.render(
8 <React.StrictMode>
9 <BrowserRouter>
10 <App />
11 </BrowserRouter>
12 </React.StrictMode>,
13 document.getElementById('root')
14 );
15 reportWebVitals();

With this in place, our application is complete! Run this command to spin up a local development server:

1npm start

The application will be available at http://localhost:3000/.

Find the complete project here on GitHub.

Conclusion

In this article, we looked at how to upload multiple files to Cloudinary. By combining this with a drag and drop API, which allows users to upload multiple files at once, we can provide a seamless experience for the user - no more uploading files one at a time!

Here are some resources you may find helpful:

Ifeoma Imoh

Software Developer

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