How to Create React Multiple File Upload using NextJS and Typescript
Jasser Mark Arioste
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:
- Select multiple images from the user's computer,
- Preview images before uploading,
- Create a backend API endpoint for file upload
- 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:
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:
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:
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