Skip to content

jerrynavi/react-file-upload

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Better File Uploads in React using axios and React Circular Progressbar

Introduction

Ever tried to upload a file? On most websites, when you click on the submit button on a file upload form, you get the feeling of being stuck in limbo because the page just loads until the upload is done. If you are uploading your file on a slow connection, what you get is

Stuck

In this guide, we will take a different approach to file uploads by displaying the progress of an upload.


Let's go ahead and bootstrap a React app using create-react-app

npx create-react-app my-app --template typescript

When the installation is completed, cd into the project directory and run the following command

yarn add axios react-circular-progressbar

to install Axios and a React progressbar component (there's tons of progress indicators for React on NPM!). Axios is our HTTP client for making requests to our app's API. We will not be concerned with the implementation details of an API at the moment, so I've gone ahead to mock responses for a successful and a failed request.

When that's done, let's go straight to writing code. Our project folder should look something like this:

├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   ├── serviceWorker.ts
│   └── setupTests.ts
├── tsconfig.json
└── yarn.lock

Open up App.tsx and replace the contents with this:

import React, { FC } from 'react';
import './App.css';

const App: FC = (): JSX.Element => {
    return (
        <div className="app">
            <div className="image-preview-box">
            </div>

            <form className="form">
                <button className="file-chooser-button" type="button">
                    Choose File
                    <input
                        className="file-input"
                        type="file"
                        name="file" />
                </button>
                <button className="upload-button" type="submit">
                    Upload
                </button>
            </form>
        </div>
    );
}

export default App;

What we have now is an empty div for previewing an uploaded image and a form setup with a file input. Let's add some CSS to make things pretty.

Pretty gif

Open the App.css file and replace the existing contents with the following:

.app {
    display: flex;
    height: 100vh;
    width: 100%;
    justify-content: center;
    align-items: center;
    flex-direction: column;
}

.image-preview-box {
    width: 200px;
    height: 200px;
    border: 1px solid rgba(0,0,0,0.3);
    text-align: center;
    display: flex;
    justify-content: center;
    align-items: center;
}

.form {
    display: flex;
    flex-direction: column;
    position: relative;
}

.form > * {
    margin: 0.5em auto;
}

.file-chooser-button {
    border: 1px solid teal;
    padding: 0.6em 2em;
    position: relative;
    color: teal;
    background: none;
}

.file-input {
    position: absolute;
    opacity: 0;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
}

.upload-button {
    background: teal;
    border: 1px solid teal;
    color: #fff;
    padding: 0.6em 2em;
}

Now let's go back to the template and set up our form to - for the purpose of this project - validate and accept images smaller than 2mb.

Add the following to the top of our component:

+ const [file, setFile] = useState();

Change the following in App.tsx:

- <input
-    className="file-input"
-    type="file"
-    name="file" />
+ <input
+    className="file-input"
+    type="file"
+    name="file"
+    accept={acceptedTypes.toString()}
+    onChange={(e) => {
+        if (e.target.files && e.target.files.length > 0) {
+            setFile(e.target.files[0])
+        }
+    }} />

We are currently accepting files that match some criteria, and saving the file to the Function Component state if it passes validation. The accept attribute value is a string that defines the file types the file input should accept. This string is a comma-separated list of unique file type specifiers. The files attribute is a FileList object that lists every selected file (only one, unless the multiple attribute is specified).1

For flexibility, you can add this array just after the last line of imports in App.tsx:

const acceptedTypes: string[] = [
    'image/png',
    'image/jpg',
    'image/jpeg',
];

Next we will import Axios and attempt to submit the user selected file to our (mock) API. Add the axios import:

+ import axios from 'axios';

and add the following code at the top of the App component:

const [uploadProgress, updateUploadProgress] = useState(0);
const [imageURI, setImageURI] = useState<string|null>(null);
const [uploadStatus, setUploadStatus] = useState(false);
const [uploading, setUploading] = useState(false);

const getBase64 = (img: Blob, callback: any) => {
    const reader = new FileReader();
    reader.addEventListener('load', () => callback(reader.result));
    reader.readAsDataURL(img);
}

const isValidFileType = (fileType: string): boolean => {
    return acceptedTypes.includes(fileType);
};

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

    if (!isValidFileType(file.type)) {
        alert('Only images are allowed (png or jpg)');
        return;
    }

    setUploading(true);
    const formData = new FormData();
    formData.append('file', file);

    axios({
        method: 'post',
        headers: {
            'Content-Type': 'multipart/form-data',
        },
        data: formData,
        url: 'http://www.mocky.io/v2/5e29b0b93000006500faf227',
        onUploadProgress: (ev: ProgressEvent) => {
            const progress = ev.loaded / ev.total * 100;
            updateUploadProgress(Math.round(progress));
        },
    })
    .then((resp) => {
        // our mocked response will always return true
        // in practice, you would want to use the actual response object
        setUploadStatus(true);
        setUploading(false);
        getBase64(file, (uri: string) => {
            setImageURI(uri);
        });
    })
    .catch((err) => console.error(err));
};

It feels like a lot is going on here, but all we are doing is

  • preventing the default form submit action
  • validating the file type using Javascript (¯_(ツ)_/¯)
  • creating a FormData object and adding the file we have in state to the object
  • submitting an axios POST request
  • getting the current upload progress and saving it as a percentage value to our app's state using axios' onUploadProgress() config option
  • marking the upload as done in our state (useful later to show our photo preview)
  • and making sure None Shall Pass™

Of course we will need to update our form to account for the new changes:

- <form className="form">
+ <form onSubmit={handleFileUpload} className="form">

We will also need to update the empty div and make it show a preview of our uploaded file:

<div className="image-preview-box">
+ {(uploadStatus && imageURI)
+     ? <img src={imageURI} alt="preview" />
+     : <span>A preview of your photo will appear here.</span>
+ }
</div>

To wrap things up, let's import our progress component and set it up. First, add the following to the app's imports:

+ import { CircularProgressbar, buildStyles } from 'react-circular-progressbar';
+ import "react-circular-progressbar/dist/styles.css";

Then add this just after the closing </form> tag:

{(uploading)
    ?
    <div className="progress-bar-container">
        <CircularProgressbar
            value={uploadProgress}
            text={`${uploadProgress}% uploaded`}
            styles={buildStyles({
                textSize: '10px',
                pathColor: 'teal',
            })}
        />
    </div>
    : null
}

All done! We have been able to inspect and show our users what happens with their upload as it happens. We can extend this further by make it possible for users to cancel their uploads2 if it's progressing slowly.

You can find the project source code here. Feel free to check it out and let me know what you think in the comments.

References

  1. HTML input element on MDN
  2. Axios docs

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published