How I built a custom image file uploader

Published: 22 July, 2024

4 mins

Read on dev.to

63 reactions

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

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>
 </>
)
}
 

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);
    }
  }

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;
}

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]);

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];
        });

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);
    });
  }
 

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
  });

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);
    }
  }

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>
  );

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.

THE END