Published on

Upload images to Cloudinary with React

Authors

Cloudinary is a cloud service that offers a solution to a web application's entire image management pipeline. Easily upload images to the cloud. Automatically perform smart image resizing, cropping and conversion without installing any complex software.

In this tutorial we will learn how to use and upload images to Cloudinary in a React application.

Sign up for Cloudinary

To get startet with Cloudinary register a free account by visiting https://cloudinary.com/users/register/free.

After validating your email address you are good to go.

Different options to upload images

You could use Cloudinary's upload widget to easily upload images from your local file system, web address and other sources. But in this tutorial I want to show you how to upload images with their upload endpoint.

The React sample is based on this Codepen and the corresponding blogpost.

Enable unsigned uploading

You first need to enable unsigned uploading in your Cloudinary account from the Upload settings page.

Clicking Enable unsigned uploading generates a random upload preset which you need to save into your environement variables together with you Cloudinary cloud name.

Enable unsigned uploadingEnable unsigned uploading

Setup sample app

We use Next.js and TailwindCSS for our sample app.

We use the with-tailwind example as a starting point. Run this:

npx create-next-app --example with-tailwindcss react-cloudinary

Then create a new file components/image-uploader.js.

components/image-uploader.js
import { useRef } from "react";

export default function ImageUploader() {
  const fileSelect = useRef(null);

  async function handleImageUpload() {
    if (fileSelect) {
      fileSelect.current.click();
    }
  }

  function handleFiles(files) {
    for (let i = 0; i < files.length; i++) {
      console.log(files[i]);
    }
  }

  return (
    <div
      className="bg-gray-200 border-4 border-dashed border-gray-400 rounded-lg"
      style={{ height: 400, width: 600 }}
    >
      <form className="flex justify-center items-center h-full">
        <div className="text-gray-700 text-center">
          <button
            className="bg-blue-600 hover:bg-blue-800 text-white font-bold px-4 py-2 rounded block m-auto"
            onClick={handleImageUpload}
            type="button"
          >
            Browse
          </button>
        </div>

        <input
          ref={fileSelect}
          type="file"
          accept="image/*"
          style={{ display: "block" }}
          onChange={(e) => handleFiles(e.target.files)}
        />
      </form>
    </div>
  );
}

At the moment this component displays a box with a button Browse to trigger the file upload. Because styling file inputs is difficult we hide it and display a button instead to trigger the file upload.

The function handleFiles will later upload the images to cloudinary. Right now we simply log the files to the console.

Trigger uploading images with Browse button

Upload function

For the upload function we need two environment variables: NEXT_PUBLIC_CLOUDINARY_CLOUDNAME and NEXT_PUBLIC_CLOUDINARY_UNSIGNED_UPLOAD_PRESET. We add them to our .env.local file and need to preset the variables with NEXT_PUBLIC to expose them to the browser.

Learn more about environment variables in Next.js here.

With this preparation add the following uploadFile function:

function uploadFile(file) {
  const url = `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUDNAME}/upload`;
  const xhr = new XMLHttpRequest();
  const fd = new FormData();
  xhr.open("POST", url, true);
  xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");

  // Update progress (can be used to show progress indicator)
  xhr.upload.addEventListener("progress", (e) => {
    setProgress(Math.round((e.loaded * 100.0) / e.total));
    console.log(Math.round((e.loaded * 100.0) / e.total));
  });

  xhr.onreadystatechange = (e) => {
    if (xhr.readyState == 4 && xhr.status == 200) {
      const response = JSON.parse(xhr.responseText);

      setImage(response.secure_url);
      console.log(response.secure_url);
    }
  };

  fd.append(
    "upload_preset",
    process.env.NEXT_PUBLIC_CLOUDINARY_UNSIGNED_UPLOAD_PRESET
  );
  fd.append("tags", "browser_upload");
  fd.append("file", file);
  xhr.send(fd);
}

This function uses the Cloudinary API endpoint upload with our cloud name and the unsigned upload preset.

We call the function for every selected file in our handleFiles function.

function handleFiles(files) {
  for (let i = 0; i < files.length; i++) {
    console.log(files[i]);
    uploadFile(files[i]);
  }
}

We use XMLHttpRequest instead of fetch for our POST request, because fetch does not support getting a progress indicator back.

To store the current upload progress and the uploaded image we need to add two useState hooks.

const [image, setImage] = useState(null);
const [progress, setProgress] = useState(0);

Our components/image-uploader.js file now looks like this:

components/image-uploader.js
import { useRef, useState } from "react";

export default function ImageUploader({ defaultImage }) {
  const fileSelect = useRef(null);
  const [image, setImage] = useState(defaultImage);
  const [progress, setProgress] = useState(0);

  async function handleImageUpload() {
    if (fileSelect) {
      fileSelect.current.click();
    }
  }

  function handleFiles(files) {
    for (let i = 0; i < files.length; i++) {
      console.log(files[i]);
      uploadFile(files[i]);
    }
  }

  function uploadFile(file) {
    const url = `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUDNAME}/upload`;
    const xhr = new XMLHttpRequest();
    const fd = new FormData();
    xhr.open("POST", url, true);
    xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");

    // Update progress (can be used to show progress indicator)
    xhr.upload.addEventListener("progress", (e) => {
      setProgress(Math.round((e.loaded * 100.0) / e.total));
      console.log(Math.round((e.loaded * 100.0) / e.total));
    });

    xhr.onreadystatechange = (e) => {
      if (xhr.readyState == 4 && xhr.status == 200) {
        const response = JSON.parse(xhr.responseText);

        setImage(response.secure_url);
        console.log(response.secure_url);
      }
    };

    fd.append(
      "upload_preset",
      process.env.NEXT_PUBLIC_CLOUDINARY_UNSIGNED_UPLOAD_PRESET
    );
    fd.append("tags", "browser_upload");
    fd.append("file", file);
    xhr.send(fd);
  }

  return (
    <>
      {image ? (
        <img
          className="object-contain rounded-lg"
          src={image.replace("upload/", "upload/w_600/")}
          style={{ height: 400, width: 600 }}
        />
      ) : (
        <div
          className="bg-gray-200 border-4 border-dashed border-gray-400 rounded-lg"
          style={{ height: 400, width: 600 }}
        >
          <form className="flex justify-center items-center h-full">
            {progress === 0 ? (
              <div className="text-gray-700 text-center">
                <button
                  className="bg-blue-600 hover:bg-blue-800 text-white font-bold px-4 py-2 rounded block m-auto"
                  onClick={handleImageUpload}
                  type="button"
                >
                  Browse
                </button>
              </div>
            ) : (
              <span className="text-gray-700">{progress}%</span>
            )}

            <input
              ref={fileSelect}
              type="file"
              accept="image/*"
              style={{ display: "none" }}
              onChange={(e) => handleFiles(e.target.files)}
            />
          </form>
        </div>
      )}
    </>
  );
}

With that we added the ability to upload images to Cloudinary to our React app. We could further improve the upload experience by allowing Drag and Drop.

Uploading images by Drag and Drop

To implement the Drag and Drop functionality we use a useEffect hook which is run only once after mounting the component. We add another DOM reference dropbox with the useRef to a container div. When dropping the files we upload them through calling our handleFiles function.

return <div ref={dropbox}>...</div>;
useEffect(() => {
  function dragEnter(e) {
    e.stopPropagation();
    e.preventDefault();
  }

  function dragOver(e) {
    e.stopPropagation();
    e.preventDefault();
  }

  function drop(e) {
    e.stopPropagation();
    e.preventDefault();

    const dt = e.dataTransfer;
    const files = dt.files;

    handleFiles(files);
  }

  dropbox.current.addEventListener("dragenter", dragEnter, false);
  dropbox.current.addEventListener("dragover", dragOver, false);
  dropbox.current.addEventListener("drop", drop, false);

  return () => {
    dropbox.current.removeEventListener("dragenter", dragEnter);
    dropbox.current.removeEventListener("dragover", dragOver);
    dropbox.current.removeEventListener("drop", drop);
  };
}, []);

Consuming images

The real benefit of uploading images to Cloudinary instead of e.g. Amazon S3 are the integrated image transforming capabilities. To display optimized images you could either use a library like cloudinary-react or just use the URL based Transformation API like in our example above.

Follow along as I'm building awesomereact.com in public.