DEV Community

Bolaji Bolajoko
Bolaji Bolajoko

Posted on

How I built a custom image file uploader

The whole problem started with some issues I was facing when trying to use Cloudinary file Uploader. After spending a few hours trying to get the setup working, which I did not, I decided to build my own custom image file uploader for the project I am working on.

At first, I had no idea how to implement this, but I knew I would figure it out somehow. (Isn't that what most developers do? 😀)

This project is a React project using Next.js with TypeScript. You might want to spin up a Next.js project to follow along, with a couple of libraries from npm to get the ball rollingâš¾.

npm i react-dropzone
Enter fullscreen mode Exit fullscreen mode

Let's create a React component called UploadImageFiles (Oops, I'm not good at naming stuff, so bear with me 😋). Here is the barebones of the component with some state we will be needing:

export default function UploadImageFiles() {
  const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
  const [fileList, setFileList] = useState<FileUpload[]>([]);
  const [fileError, setFileError] = useState<string | null>(null);

return (
 <>
    <main>
       {/* some UIish 😄 JSX*/}
    </main>
 </>
)
}

Enter fullscreen mode Exit fullscreen mode

Just to move this further, I like to break down my thought process of the implementation into a list. You can call it a pseudocode. 🤷

  • Handle user file selection
  • Validate the file
  • Display the file list (I call this staging 🎬)

Let’s handle the file selection with the help of TypeScript so that we won’t go broke. 😄

 function handleFileChange(e: ChangeEvent<HTMLInputElement>) {
    const inputFiles = Array.from(e.target.files || []);

    const validatedFiles: File[] = [];

    files.forEach((file) => {
      if (!file.type.startsWith("image/")) { // check for valid image file
        setFileError("File should be an image"); 
      } else if (file.size > 1024 * 1024 * 25) { // check for image size
        setFileError("File max size is 24MB");
      } else {
        validatedFiles.push(file);
      }
    });
    if (validatedFiles.length > 0) {
      setSelectedFiles(validatedFiles);
      setFileError(null);
    }
  }
Enter fullscreen mode Exit fullscreen mode

The handleFileChange() function helps with file selection and validates the files for valid file type and the image file size we are expecting. If not, the fileError state will be set. If our checks all pass, then we push the file into the validatedFiles array. The last part of the code sets the selectedFiles state using the setSelectedFiles action with the validatedFiles list.

🚫🚫🚫 Where is FileUpload?

Here you go:

interface FileUpload {
  url: string;
  name: string;
  size: number;
}
Enter fullscreen mode Exit fullscreen mode

With the file selection and validation out of the way, let’s do some staging…🎬

To add new files to the staging area, our staging function is going to be in a useEffect so that new files are added when selected.

useEffect(() => {
    function stagingFiles() {
      if (selectedFiles.length) {
        const imageFile = selectedFiles.map((file: File) => {
          return {
            url: URL.createObjectURL(file),
            name: file.name,
            size: file.size,
          };
        });
        setFileList((curState) => {
          const newImageFile = imageFile.filter((file) => !curState.some((item) => item.name === file.name))
          return [...curState, ...newImageFile];
        });
      }
    }
    stagingFiles();
  }, [selectedFiles]);
Enter fullscreen mode Exit fullscreen mode

In the staging area, we need the image URL, name, and size. We use the URL to display the image. Since our selectedFiles is just a list of images, we map through it and return the properties we need (as mentioned above) from it as an object. URL.createObjectURL() helps to create the image URL. createObjectURL().

setFileList((curState) => {
          const newImageFile = imageFile.filter((file) => !curState.some((item) => item.name === file.name))
          return [...curState, ...newImageFile];
        });
Enter fullscreen mode Exit fullscreen mode

The second part of the staging function is setting a distinct image, ensuring no duplicate image file is added to the staging area.

Do not forget to add the selectedFiles to the useEffect dependency array.

We might need to remove some files from the staging area. The method is quick and short:

function removeFromList(item: number) {
    setFileList((curState) => {
      return curState.filter((_, index) => index !== item);
    });
  }

Enter fullscreen mode Exit fullscreen mode

You might be wondering, where is our drag ‘n drop logic. Just a moment…

const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop: (files: File[]) => {
      processFiles(files);
    },
    multiple: true,
    noClick: true, // avoid duplicate opening of file selection ui
  });
Enter fullscreen mode Exit fullscreen mode

React dropzone provides us with the useDropzone hook to perform our drag-and-drop feature. From the hook, we destructure the properties we need. You can check out their official documentation for more info: react-dropzone.

We will be making some updates to our handleFileChange function by moving part of it into a processFiles function:

 function processFiles(files: File[]) {
    const validatedFiles: File[] = [];

    files.forEach((file) => {
      if (!file.type.startsWith("image/")) {
        setFileError("File should be an image");
      } else if (file.size > 1024 * 1024 * 25) {
        setFileError("File max size is 24MB");
      } else {
        validatedFiles.push(file);
      }
    });
    if (validatedFiles.length > 0) {
      setSelectedFiles(validatedFiles);
      setFileError(null);
    }
  }
Enter fullscreen mode Exit fullscreen mode

One last couple of changes before closing. Add getRootProps and getInputProps to the root element and input element respectively:

  return (
    <div>
      <div {...getRootProps()}>
        <input {...getInputProps()} onChange={handleFileChange} />
        {isDragActive ? <p>Drop the files here ...</p> : <p>Drag 'n' drop some files here, or click to select files</p>}
      </div>
      {fileError && <p>{fileError}</p>}
      <ul>
        {fileList.map((file, index) => (
          <li key={index}>
            <img src={file.url} alt={file.name} width={50} />
            <button onClick={() => removeFromList(index)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

Here is my current version of the implementation.

Image description

Hope you had much fun as I do. Feel free to implement your own features.

Top comments (1)

Collapse
 
colbyfayock profile image
Colby Fayock

Nice job with this! Curious what you struggled with to see if we (Cloudinary) can make it better

Also did you happen to check out our Nextjs SDK? Has an upload component