React Typescript – Multiple Image Upload example (with Preview)

In this tutorial, I will show you way to Upload Image Image (with Preview) to a Rest API using React Typescript and Hooks. The React App uses Axios and Multipart File for making HTTP requests, Bootstrap for progress bar. You also have a display list of images’ information (with download url).

More Practice:
React Typescript CRUD example
React Hook Form Typescript example with Validation
React Custom Hook in Typescript example
React Query and Axios (Typescript) example
Spring Boot + React Typescript example

Serverless with Firebase:
React Typescript Firebase CRUD with Realtime Database
React Typescript Firestore CRUD example with Cloud Firestore


Overview of React Typescript Multiple Image upload example

We’re gonna create a React Typescript functional component (using Hooks) for Multiple Images Upload with Preview application in that user can:

  • see the preview of images that will be uploaded
  • see the upload process (percentage) of each image with progress bars
  • view all uploaded files
  • download link to file when clicking on the file name

Here are screenshots of our React App:

– Before uploading multiple Images, we can see the preview:

react-typescript-multiple-image-upload-with-preview-example

– Upload is processed and finished with Progress bars:

react-typescript-multiple-image-upload-with-progress-bar

– List of Images Display with download Urls:

react-typescript-multiple-image-upload-display

– Show status for each image upload:

react-typescript-multiple-image-upload-with-preview

Technology

  • React 18/17/16
  • Axios 0.27.2
  • Bootstrap 4

Rest API for Image Upload & Storage

Here are APIs that we will use Axios to make HTTP requests:

Methods Urls Actions
POST /upload upload a File
GET /files get List of Files (name & url)
GET /files/[filename] download a File

You can find how to implement the Rest APIs Server at one of following posts:
Node Express File Upload Rest API example
Node Express File Upload to MongoDB example
Node Express File Upload to Google Cloud Storage example
Spring Boot Multipart File upload (to static folder) example

Project Structure

After building the React Typescript project is done, the folder structure will look like this:

react-typescript-multiple-image-upload-project

Let me explain it briefly.

File exports IFile interface.
FileUploadService provides methods to save File and get Files using Axios.
ImagesUpload is a React Typescript functional component contains multiple image upload form, preview, progress bar, display of list of images with download url.
App.tsx is the container that we embed all React components.

http-common.ts initializes Axios with HTTP base Url and headers.
– We configure port for our App in .env

Setup React Typescript Multiple Image Upload Project

Open cmd at the folder you want to save Project folder, run command:
npx create-react-app react-typescript-multiple-image-upload --template typescript
Or: yarn create react-app react-typescript-multiple-image-upload --template typescript

After the process is done. We create additional folders and files like the following tree:


public
src
components
ImagesUpload.tsx
services
FileUploadService.ts
types
File.ts
App.css
App.tsx
index.tsx
package.json

Import Bootstrap to React Typescript Multiple Image Upload App

Run command: npm install [email protected].
Or: yarn add [email protected]

Open src/App.tsx and modify the code inside it as following-

import "bootstrap/dist/css/bootstrap.min.css";
import "./App.css";

const App: React.FC = () => {
  return (
    ...
  );
}

export default App;

Initialize Axios for React Typescript Client

Let’s install axios with command: npm install [email protected].
Or: yarn add [email protected]

Under src folder, we create http-common.ts file with following code:

import axios from "axios";

export default axios.create({
  baseURL: "http://localhost:8080",
  headers: {
    "Content-type": "application/json",
  },
});

You can change the baseURL that depends on REST API url that your Server configures.

Create Service for File Upload

This service will use Axios to send HTTP requests.
There are 2 functions:

  • upload(file): POST form data with a callback for tracking upload progress
  • getFiles(): GET list of Files’ information

services/FileUploadService.ts

import http from "../http-common";

const upload = (file: File, onUploadProgress: any): Promise<any> => {
  let formData = new FormData();

  formData.append("file", file);

  return http.post("/upload", formData, {
    headers: {
      "Content-Type": "multipart/form-data",
    },
    onUploadProgress,
  });
};

const getFiles = () : Promise<any> => {
  return http.get("/files");
};

const FileUploadService = {
  upload,
  getFiles,
};

export default FileUploadService;

– First we import Axios as http from http-common.ts.

– Inside upload() method, we use FormData to store key-value pairs. It helps to build an object which corresponds to HTML form using append() method.

– We pass onUploadProgress to exposes progress events. This progress event are expensive (change detection for each event), so you should only use when you want to monitor it.

– We call Axios post() to send an HTTP POST for uploading a File to Rest APIs Server and get() method for HTTP GET request to retrieve all stored files.

Create File Interface

We will export a IFile type with 2 properties: url and name.

types/File.ts

export default interface IFile {
  url: string,
  name: string,
}

Create React Page for Upload Multiple Images

Let’s create a Multiple Image Upload UI with Progress Bar, Card, Button and Message.

First we create a React template with React Hooks (useState, useEffect) and import FileUploadService:

components/ImagesUpload.tsx

import { useState, useEffect, useRef } from "react";
import UploadService from "../services/FileUploadService";

const ImagesUpload: React.FC = () => {
  
  return (
    
  );
};

export default ImagesUpload;

Then we define the state using React Hooks:

...
import IFile from "../types/File";

const ImagesUpload: React.FC = () => {
  const [selectedFiles, setSelectedFiles] = useState<FileList | null>(null);
  const [imagePreviews, setImagePreviews] = useState<Array<string>>([]);
  const [progressInfos, setProgressInfos] = useState<Array<ProgressInfo>>([]);
  const [message, setMessage] = useState<Array<string>>([]);
  const [imageInfos, setImageInfos] = useState<Array<IFile>>([]);
  const progressInfosRef = useRef<any>(null);

  ...
}

We will use URL.createObjectURL() to get the image preview URL and put it into imagePreviews array. This method produces a DOMString including a URL describing the object provided in the parameter. The URL life is tied to the document in the window on which it was created.

Next we define selectImages() function which helps us to get the selected Images from <input type="file" ...> element later.

const ImagesUpload: React.FC = () => {
  ...
  const selectImages = (event: React.ChangeEvent<HTMLInputElement>) => {
    let images: Array<string> = [];
    let files = event.target.files;

    if (files) {
      for (let i = 0; i < files.length; i++) {
        images.push(URL.createObjectURL(files[i]));
      }
  
      setSelectedFiles(files);
      setImagePreviews(images);
      setProgressInfos([]);
      setMessage([]);
    }
  };
  ...
}

We use selectedFiles array for accessing current selected Files. Every file has a corresponding progress Info (percentage, file name) and index in progressInfos array.

const ImagesUpload: React.FC = () => {
  ...

  const uploadImages = () => {
    if (selectedFiles != null) {
      const files = Array.from(selectedFiles);

      let _progressInfos = files.map((file) => ({
        percentage: 0,
        fileName: file.name
      }));

      progressInfosRef.current = _progressInfos;

      const uploadPromises = files.map((file, i) => upload(i, file));

      Promise.all(uploadPromises)
        .then(() => UploadService.getFiles())
        .then((images) => {
          setImageInfos(images.data);
        });

      setMessage([]);
    }
  };
  ...
}

In the code above, for each image file, we invoke upload() function which call UploadService.upload() method with an index (i) parameter and return a Promise.

After all the processes are done, we call UploadService.getFiles() to get the images’ information and assign the result to imageInfos state, which is an array of {name, url} objects.

So create following upload() function like this:

const ImagesUpload: React.FC = () => {
  ...

  const upload = (idx: number, file: File) => {
    let _progressInfos = [...progressInfosRef.current];
    return UploadService.upload(file, (event) => {
      _progressInfos[idx].percentage = Math.round(
        (100 * event.loaded) / event.total
      );
      setProgressInfos(_progressInfos);
    })
      .then(() => {
        setMessage((prevMessage) => [
          ...prevMessage,
          file.name + ": Successful!"
        ]);
      })
      .catch((err: any) => {
        _progressInfos[idx].percentage = 0;
        setProgressInfos(_progressInfos);

        let msg = file.name + ": Failed!";
        if (err.response && err.response.data && err.response.data.message) {
          msg += " " + err.response.data.message;
        }

        setMessage((prevMessage) => [
          ...prevMessage,
          msg
        ]);
      });
  };
  ...
}

The progress will be calculated basing on event.loaded and event.total and percentage value is assigned to specific item of progressInfos by the index.

We also need to do this work in the Effect Hook useEffect() method which serves the same purpose as componentDidMount():

const ImagesUpload: React.FC = () => {
  ...

  useEffect(() => {
    UploadService.getFiles().then((response) => {
      setImageInfos(response.data);
    });
  }, []);

  ...
}

Now we return the Multiple Image Upload UI. Add the following code inside return() block:

const ImagesUpload: React.FC = () => {
  ...
  return (
    <div>
      {progressInfos &&
        progressInfos.length > 0 &&
        progressInfos.map((progressInfo: ProgressInfo, index: number) => (
          <div className="mb-2" key={index}>
            <span>{progressInfo.fileName}</span>
            <div className="progress">
              <div
                className="progress-bar progress-bar-info"
                role="progressbar"
                aria-valuenow={progressInfo.percentage}
                aria-valuemin={0}
                aria-valuemax={100}
                style={{ width: progressInfo.percentage + "%" }}
              >
                {progressInfo.percentage}%
              </div>
            </div>
          </div>
        ))}

      <div className="row my-3">
        <div className="col-8">
          <label className="btn btn-default p-0">
            <input type="file" multiple accept="image/*" onChange={selectImages} />
          </label>
        </div>

        <div className="col-4">
          <button
            className="btn btn-success btn-sm"
            disabled={!selectedFiles}
            onClick={uploadImages}
          >
            Upload
          </button>
        </div>
      </div>

      {imagePreviews && (
        <div>
          {imagePreviews.map((img, i) => {
            return (
              <img className="preview" src={img} alt={"image-" + i} key={i} />
            );
          })}
        </div>
      )}

      {message.length > 0 && (
        <div className="alert alert-secondary mt-2" role="alert">
          <ul>
            {message.map((item, i) => {
              return <li key={i}>{item}</li>;
            })}
          </ul>
        </div>
      )}

      {imageInfos.length > 0 && (
        <div className="card mt-3">
          <div className="card-header">List of Images</div>
          <ul className="list-group list-group-flush">
            {imageInfos.map((img, index) => (
              <li className="list-group-item" key={index}>
                <p>
                  <a href={img.url}>{img.name}</a>
                </p>
                <img src={img.url} alt={img.name} height="80px" />
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

In the code above, we use Bootstrap Progress Bar:

  • .progress as a wrapper
  • inner .progress-bar to indicate the progress
  • .progress-bar requires style to set the width by percentage
  • .progress-bar also requires role and some aria attributes to make it accessible
  • label of the progress bar is the text within it

To display List of uploaded images, we iterate over imageInfos array using map() function. On each file item, we use img.url as href attribute and img.name for showing text.

The preview of uploading image is shown by iterating previewImages array.

Don’t forget to export the function component:

const ImagesUpload: React.FC = () => {
  ...
}

export default ImagesUpload;

You can simplify import statement with:
Absolute Import in React

Add Multiple Image Upload Component to App Component

Open App.tsx, import and embed the ImagesUpload Component tag.

import "bootstrap/dist/css/bootstrap.min.css";
import "./App.css";

import ImagesUpload from "./components/ImagesUpload";

const App: React.FC = () => {
  return (
    <div className="container" style={{ width: "600px" }}>
      <div className="my-3">
        <h3>bezkoder.com</h3>
        <h4>React Typescript Multiple Images Upload</h4>
      </div>

      <ImagesUpload />
    </div>
  );
}

export default App;

Configure Port for Multiple Image Upload App

Because most of HTTP Server use CORS configuration that accepts resource sharing restricted to some sites or ports, and if you use the Project in next section for making Server, you need to configure port for our App.

In project folder, create .env file with following content:

PORT=8081

So our app will run at port 8081.

Run the App

You can find how to implement the Rest APIs Server at one of following posts:
Node Express File Upload Rest API example
Node Express File Upload to MongoDB example
Node Express File Upload to Google Cloud Storage example
Spring Boot Multipart File upload (to static folder) example

Run this React Multiple Image Upload App with command: npm start.
Or: yarn start

Open Browser with url http://localhost:8081/ and check the result.

Conclusion

Today we’re learned how to build a React Typescript application for upload Multiple Images (with Preview) using Hooks, Axios, Bootstrap with Progress Bar. We also provide the ability to show list of files, upload progress percentage, and to download file from the server.

Happy Learning! See you again.

Further Reading

React Typescript CRUD example
React Hook Form Typescript example with Validation
React Custom Hook in Typescript example
React Query and Axios (Typescript) example
Spring Boot + React Typescript example

Serverless with Firebase:
React Typescript Firebase CRUD with Realtime Database
React Typescript Firestore CRUD example with Cloud Firestore

Fullstack:
React + Spring Boot + MySQL: CRUD example
React + Spring Boot + PostgreSQL: CRUD example
React + Spring Boot + MongoDB: CRUD example
React + Node + Express + MySQL: CRUD example
React + Node + Express + PostgreSQL example
React Redux + Node + Express + MySQL: CRUD example
React + Node + Express + MongoDB exampleReact + Django + Rest Framework example

Source Code

You can find the complete source code for this tutorial at Github.