Skip to content

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

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.

Before getting into the specifics of the drag-and-drop API, let’s understand the basic mechanics of drag-and-drop interactions. To move a file from one location to another, typically, you:

  1. Move your mouse while holding down the left or right mouse button.
  2. Bring the mouse over to the destination folder or area.
  3. Release the mouse button to drop the file.

This interaction is the foundation of the drag-and-drop functionality we will be implementing.

Here is a link to the demo on CodeSandbox.

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:

    git clone https://github.com/ifeoma-imoh/drag-and-drop-demo
    
    cd drag-and-drop-demo
    
    npm install
Code language: PHP (php)

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

    npm i axios react-router-dom@6 cloudinary-react
Code language: CSS (css)

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:

    REACT_APP_CLOUD_NAME = 'INSERT YOUR CLOUD NAME HERE' 
    REACT_APP_UPLOAD_PRESET = 'INSERT YOUR UNSIGNED UPLOAD PRESET KEY HERE'
Code language: JavaScript (javascript)

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:

    cp .env .env.local
Code language: CSS (css)

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:

    export const cloudName = process.env.REACT_APP_CLOUD_NAME;
    export const uploadPreset = process.env.REACT_APP_UPLOAD_PRESET;
Code language: JavaScript (javascript)

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:

    import axios from 'axios';
    import { cloudName, uploadPreset } from './cloudinaryConfig';
    export const uploadImage = ({ file, successCallback }) => {
      const url = `https://api.cloudinary.com/v1_1/${cloudName}/image/upload`;
      const data = new FormData();
      data.append('file', file);
      data.append('upload_preset', uploadPreset);
      axios
        .post(url, data, {
          headers: {
            'Content-Type': 'multipart/form-data',
          },
        })
        .then((response) => successCallback(response.data));
    };
Code language: JavaScript (javascript)

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.

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:

    import { message } from 'antd';
    import { useState } from 'react';
    import { uploadImage } from '../utility/api';
    import { useNavigate } from 'react-router-dom';
    const useFileSelection = () => {
      const [selectedFiles, setSelectedFiles] = useState([]);
      const [isUploading, setIsUploading] = useState(false);
      const navigate = useNavigate();
      const addFile = (file) => {
        setSelectedFiles((currentSelection) => [...currentSelection, file]);
      };
      const removeFile = (file) => {
        setSelectedFiles((currentSelection) => {
          const newSelection = currentSelection.slice();
          const fileIndex = currentSelection.indexOf(file);
          newSelection.splice(fileIndex, 1);
          return newSelection;
        });
      };
      const uploadSelection = () => {
        if (selectedFiles.length === 0) {
          message.error('You need to select at least one image');
          return;
        }
        setIsUploading(true);
        const uploadResults = [];
        selectedFiles.forEach((file) => {
          uploadImage({
            file,
            successCallback: (response) => {
              uploadResults.push(response);
              if (uploadResults.length === selectedFiles.length) {
                setIsUploading(false);
                message.success('Images uploaded successfully');
                navigate('/results', { state: uploadResults });
              }
            },
          });
        });
      };
      return [addFile, removeFile, isUploading, uploadSelection];
    };
    export default useFileSelection;
Code language: JavaScript (javascript)

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.

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:

    import useFileSelection from '../hooks/useFileSelection';
    import { Button, Card } from 'antd';
    import DragAndDrop from './DragAndDrop';
    const ImageUpload = () => {
      const [addFile, removeFile, isUploading, uploadSelection] =
        useFileSelection();
      return (
        <Card
          style={{ margin: 'auto', width: '50%' }}
          actions={[
            <Button type="primary" loading={isUploading} onClick={uploadSelection}>
              Submit
            </Button>,
          ]}
        >
          <DragAndDrop addFile={addFile} removeFile={removeFile} />
        </Card>
      );
    };
    export default ImageUpload;
Code language: JavaScript (javascript)

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.

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:

    import { Card } from 'antd';
    import { Image as CloudinaryImage, Transformation } from 'cloudinary-react';
    import { cloudName, uploadPreset } from '../utility/cloudinaryConfig';
    const { Meta } = Card;
    const Image = ({ publicId, originalFilename, createdAt }) => {
      const dateString = new Intl.DateTimeFormat('en-GB', {
        dateStyle: 'full',
        timeStyle: 'long',
      }).format(new Date(createdAt));
      return (
        <Card
          hoverable
          style={{ width: 240 }}
          cover={
            <CloudinaryImage
              publicId={`${publicId}.png`}
              cloudName={cloudName}
              upload_preset={uploadPreset}
              secure={true}
              alt={originalFilename}
            >
              <Transformation width={240} height={300} crop="scale" />
            </CloudinaryImage>
          }
        >
          <Meta
            title={originalFilename}
            description={`Uploaded on ${dateString}`}
          />
        </Card>
      );
    };
    export default Image;
Code language: JavaScript (javascript)

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.

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:

    import { Row, Col, PageHeader } from 'antd';
    import { useEffect, useState } from 'react';
    import { useLocation, useNavigate } from 'react-router-dom';
    import Image from './Image';
    const ImageGrid = () => {
      const location = useLocation();
      const navigate = useNavigate();
      const [resources, setResources] = useState([]);
      useEffect(() => {
        const uploadResults = location.state;
        setResources(uploadResults);
      }, []);
      const returnToUploads = () => {
        navigate('/');
      };
      return (
        <div style={{ margin: '5%' }}>
          <PageHeader
            ghost={true}
            onBack={returnToUploads}
            title="Upload Pictures"
          />
          <Row gutter={[0, 16]} align="middle">
            {resources.map((image) => {
              const { original_filename, public_id, created_at } = image;
              return (
                <Col span={6}>
                  <Image
                    key={public_id}
                    publicId={public_id}
                    originalFilename={original_filename}
                    createdAt={created_at}
                  />
                </Col>
              );
            })}
          </Row>
        </div>
      );
    };
    export default ImageGrid;
Code language: JavaScript (javascript)

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.

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:

    import ImageUpload from '../components/ImageUpload';
    import ImageGrid from '../components/ImageGrid';
    const routes = [
      {
        path: '/',
        element: <ImageUpload />,
      },
      {
        path: '/results',
        element: <ImageGrid />,
      },
    ];
    export default routes;
Code language: JavaScript (javascript)

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 :

    import './App.css';
    import { useRoutes } from 'react-router-dom';
    import routes from './utility/routes';
    const App = () => {
      const router = useRoutes(routes);
      return <div style={{ margin: '1%' }}>{router}</div>;
    };
    export default App;
Code language: JavaScript (javascript)

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:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import App from './App';
    import reportWebVitals from './reportWebVitals';
    import { BrowserRouter } from 'react-router-dom';
    ReactDOM.render(
      <React.StrictMode>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </React.StrictMode>,
      document.getElementById('root')
    );
    reportWebVitals();
Code language: JavaScript (javascript)

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

    npm start

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

Find the complete project here on GitHub.

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:

Back to top

Featured Post