ReactHustle

How to Create React Multiple File Upload using NextJS and Typescript

Jasser Mark Arioste

Jasser Mark Arioste

How to Create React Multiple File Upload using NextJS and Typescript

Hello, hustlers! In this tutorial, you'll learn step-by-step how to create a react multiple file upload component in a NextJS project. 

Tutorial objectives #

In this tutorial, you'll create an image upload component that can do the following:

  1. Select multiple images from the user's computer,
  2. Preview images before uploading, 
  3. Create a backend API endpoint for file upload
  4. Write the uploaded images to the local file system.

Ultimately, to upload multiple files, I find that it's best to iterate through the selected files from an <input type="file/> component and create a new FormData that contains each file. You'll see how to do this later in the tutorial. 

For the backend, you won't use libraries to handle multer, or formidable to handle multipart/form-data because the NextJS backend is enough to process the data.

As a limitation, since I don't have a dedicated file storage service, you'll upload the files to the local file system. As a result, you'll only be able to try the demo in your local environment.

For the frontend, you'll use TailwindCSS to style our components and also use Typescript for readability and maintainability.

Final Output #

Here's the final output of what you'll be making today:

React multiple file upload

Once the files are uploaded, you can access them on the public URL for example: http://localhost:3000/test.png.

Step 0 - Project Setup #

First, let's create a new NextJS project by running the command

npx create-next-app --ts --tailwind --use-yarn --app next-multiple-file-upload

The --ts option will configure your project to use Typescript, it will create a tsconfig.json file and set all the initial files to have .ts or .tsx file extension.

The --tailwind option will configure your project to use TailwindCSS. It will create postcss.config.js and tailwind.config.js files. It will also set the globals.css file to use the tailwind directives. 

The --use-yarn option will explicitly tell the CLI to bootstrap the application using yarn. This will use yarn.lock file instead of package-lock.json file. This is my personal preference but you can use other options like --use-npm or --use-pnpm.

The --app option will tell the CLI to use the /app directory instead of the pages directory.

After that you can start the local server by running the following commands:

cd next-multiple-file-upload
yarn dev #npm run dev

For the next few steps, we'll be building the UI/UX part of the application.

Step 1 - Creating a CustomFileSelector #

Let's create a CustomFileSelector Component using tailwind classes. First, create the file components/CustomFileSelector.tsx

// components/CustomFileSelector.tsx
import classNames from "classnames";
import React, { ComponentPropsWithRef } from "react";

type Props = ComponentPropsWithRef<"input">;

const CustomFileSelector = (props: Props) => {
  return (
    <input
      {...props}
      type="file"
      multiple
      className={classNames({
        // Modify the Button shape, spacing, and colors using the `file`: directive
        // button colors
        "file:bg-violet-50 file:text-violet-500 hover:file:bg-violet-100": true,
        "file:rounded-lg file:rounded-tr-none file:rounded-br-none": true,
        "file:px-4 file:py-2 file:mr-4 file:border-none": true,
        // overall input styling
        "hover:cursor-pointer border rounded-lg text-gray-400": true,
      })}
    />
  );
};

export default CustomFileSelector;
1234567891011121314151617181920212223242526

In line 2, You import the classnames package so that you can use it later when organizing the classes.

In line 5, You copy all the props of the HTMLInputElement by using the ComponentPropsWithRef utility type so that when you use the <CustomFileSelector/> we can pass it props like onChange an value.

In line 10, the type="file" option will enable the input element to select files from the user's computer.

In line 11, the multiple property allows it to select multiple files as opposed to only one file.

In lines 12-20, you use multiple tailwind classes to style the component.

Now, let's modify the app/page.tsx file to use the CustomFileSelector component.

// app/page.tsx
import CustomFileSelector from "@/components/CustomFileSelector";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <CustomFileSelector />
    </main>
  );
}
12345678910

The output should look like this:

CustomFileUpload component

Step 2 - Previewing the Selected Images #

Creating the Component

Next, let's create a component to preview the selected images and create the file components/ImagePreview.tsx.

// components/ImagePreview.tsx
import React from "react";
import Image from "next/image";

type Props = {
  images: File[];
};

const ImagePreview = ({ images }: Props) => {
  return (
    <div>
      <div className="grid grid-cols-12 gap-2 my-2">
        {images.map((image) => {
          const src = URL.createObjectURL(image);
          return (
            <div className="relative aspect-video col-span-4" key={image.name}>
              <Image src={src} alt={image.name} className="object-cover" fill />
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default ImagePreview;
1234567891011121314151617181920212223242526

First of all, this is a display-only component that uses an array of File objects. In line 12, you use a grid layout to display the images. In lines 13-20, you iterate through the File array and get the URL by using URL.createObjectURL method. The URL.createObjectURL is a very convenient method to get file URLs from Files or Blobs. Then we just use the next/image component to render the images.

Wiring the components together

Next, let's create a container Component that uses both of the components you just created so that it can listen to the onChange event from the CustomFileSelector comopnent and pass the selected files to the ImagePreview component. 

Create the file components/FileUploadForm.tsx:

"use client"; // Make this component a client component
// components/FileUploadForm.tsx
import React, { useState } from "react";
import CustomFileSelector from "./CustomFileSelector";
import ImagePreview from "./ImagePreview";

const FileUploadForm = () => {
  const [images, setImages] = useState<File[]>([]);
  const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files) {
      //convert `FileList` to `File[]`
      const _files = Array.from(e.target.files);
      setImages(_files);
    }
  };
  return (
    <form className="w-full">
      <CustomFileSelector
        accept="image/png, image/jpeg"
        onChange={handleFileSelected}
      />
      <ImagePreview images={images} />
    </form>
  );
};

export default FileUploadForm;
123456789101112131415161718192021222324252627

We use the "use client" directive to mark this component as a client component in NextJS. Otherwise, you won't be able to use hooks like useState.

On line 7, you create a images state to store the selected files using the useState hook. You use a generic parameter to indicate that this state is of File[] type. Otherwise, it will use the never[] type and result in a type error later when we use the setImages function.

On line 8, we create a handler handleFileSelected to listen for value changes in the CustomFileSelector component. First, you check if there are selected files using the e.target.files property which is a FileList object.

However, the FileList object is hard to work with so we transform it into a File[] using Array.from method. Then, you just set the state using the setImages function.

On line 22, we use the ImagePreview component to display the selected images.

Modify app/page.tsx

Next, let's modify the /app/page.tsx file to use the FileUploadForm component.

// app/page.tsx
import FileUploadForm from "@/components/FileUploadForm";

export default function Home() {
  return (
    <main className="flex min-h-screen p-24">
      <FileUploadForm />
    </main>
  );
}
12345678910

After this step, the output should be like the screenshot below once you select files from your computer:

React multiple file upload form after selecting three images

Step 3 - Adding an Upload Button #

Next, let's add an Upload or "submit" button to our form.

// components/FileUploadForm.tsx
// ...

const FileUploadForm = () => {
  // ...
  return (
    <form className="w-full">
      <div className="flex justify-between">
        <CustomFileSelector
          accept="image/png, image/jpeg"
          onChange={handleFileSelected}
        />
        <button
          type="submit"
          className="bg-violet-50 text-violet-500 hover:bg-violet-100 px-4 py-2 rounded-md"
        >
          Upload
        </button>
      </div>
      <ImagePreview images={images} />
    </form>
  );
};

export default FileUploadForm;
12345678910111213141516171819202122232425

In line 7, you use the flex layout with justify-between class to line up both CustomFileSelector and the submit button.

In lines 12-17, you use a regular button element with tailwind CSS styling.

Step 4 - Handling onSubmit Functionality #

Next, modify the FileUploadForm component to handle the form submission:

// components/FileUploadForm.tsx
"use client"; // Make this component a client component
import React, { FormEvent, useState } from "react";
import CustomFileSelector from "./CustomFileSelector";
import ImagePreview from "./ImagePreview";
import axios from "axios";
import classNames from "classnames";

const FileUploadForm = () => {
  const [images, setImages] = useState<File[]>([]);
  const [uploading, setUploading] = useState(false);
  const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files) {
      //convert `FileList` to `File[]`
      const _files = Array.from(e.target.files);
      setImages(_files);
    }
  };

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    const formData = new FormData();
    images.forEach((image, i) => {
      formData.append(image.name, image);
    });
    setUploading(true);
    await axios.post("/api/upload", formData);
    setUploading(false);
  };
  return (
    <form className="w-full" onSubmit={handleSubmit}>
      <div className="flex justify-between">
        <CustomFileSelector
          accept="image/png, image/jpeg"
          onChange={handleFileSelected}
        />
        <button
          type="submit"
          className={classNames({
            "bg-violet-50 text-violet-500 hover:bg-violet-100 px-4 py-2 rounded-md":
              true,
            "disabled pointer-events-none opacity-40": uploading,
          })}
          disabled={uploading}
        >
          Upload
        </button>
      </div>
      <ImagePreview images={images} />
    </form>
  );
};

export default FileUploadForm;
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455

First, in line 5, you import axios package to later send a post request to the upload API endpoint that you'll create later.

In lines 20-30, you define an asynchandleSubmit callback to handle the file upload.

In line 21, the e.preventDefault() method is used so that the form won't send a POST request to the current URL/route.

In line 23, You create a new FormData object. The FormData object provides a way to easily construct a set of key/value pairs representing form fields and their values which can be easily sent using the axios.post() method. 

In lines 24-26, You iterate through the selected images and use formData.append() method to set the key/value pairs.

In lines 27-29, You set the uploading state to true, and call the axios.post() method. The axios.post method accepts a URL as the first parameter and the data as the second parameter. After everything is uploaded, the uploading state is then set to false.

Step 5 - Implementing the Upload API #

Currently, when you click the Upload button, it doesn't do anything yet because the /api/upload endpoint will return 404 status. In this step, you'll create the /api/upload endpoint to save the selected images in the local /public directory of our project.

In production apps, you should save the images in a file storage service like AWS S3 or MongoDB GridFS.

Create the file /app/api/upload/route.ts:

import fs from "fs";
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  const formData = await req.formData();
  const formDataEntryValues = Array.from(formData.values());
  for (const formDataEntryValue of formDataEntryValues) {
    if (typeof formDataEntryValue === "object" && "arrayBuffer" in formDataEntryValue) {
      const file = formDataEntryValue as unknown as Blob;
      const buffer = Buffer.from(await file.arrayBuffer());
      fs.writeFileSync(`public/${file.name}`, buffer);
    }
  }
  return NextResponse.json({ success: true });
}
123456789101112131415

In line 4, we export an async function POST which is a convention in NextJS 13 route.ts files that indicates that the route will be able to handle POST requests.

It accepts a Request object for the first argument which extends the Web Request API. This gives you helpful methods like formData(), blob(), json(), arrayBuffer() to easily process the data. This is not the case when using the `/pages/api` request handlers.

In line 5, you can easily get the formData by using the req.formData() method. Then in line 6, you transform the formData values into an array of FormDataEntryValue objects. A FormDataEntryValue can either be of type string or Blob

In line 7, we iterate through all the formDataEntryValues. At this point,  the type for each formDataEntryValue is still unknown so you have to use a guard on line 8.

On line 9, you're now sure that the formDataEntryValue is of type Blob so we use "type assertion" using the as operator. Now, you can use the methods of the Blob object such as arrayBuffer().

On line 10, you use the file.arrayBuffer() method which returns a Promise<ArrayBuffer> object. Next, you transform it into a Buffer object by using the Buffer.from method. The Buffer.from method accepts either a String, Array, Buffer, ArrayBuffer and transforms it into a Buffer object.

On line 11, Since you already have the Buffer as data, you use fs.writeFileSync to write the image into the public directory. Note that in production apps, you should upload the file to a file storage service.

Finally, on line 14, we return a JSON response.

We were able to process the request without the help of external libraries and I find using this technique incredibly easy as opposed to the /pages/api directory.

That's basically it!

Full Code and Demo #

You can check out the full code on Github: jmarioste/next-multiple-file-upload-tutorial

For the demo, you can run the project locally by running the command:

npx create-next-app -e https://github.com/jmarioste/next-multiple-file-upload-tutorial file-upload-demo
cd file-upload-demo
yarn dev

Conclusion #

You learned how to create a full feature to upload multiple files using React, NextJS, and TailwindCSS. Using the NextJS 13 /app router makes processing file upload request incredibly easy and straightforward.

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on GitHub.

Credits: Image by John Lee from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle