Comments

React file upload: proper and easy way, with NodeJS!

Upload page reloads on submitting a file for upload. Are you a newbie to React, and using this generic style to upload files on the web?

There’s a better way to handle uploads in React.

This tutorial is the answer!

Today, it’ll change forever if you go through this tutorial and implement it on your site.

We’ll use Node with React to upload multiple files at once. As we go along, there will be simple client-side validation and finally with uploaded notification can be shown with react-toastify.

Like always, start a react app with create-react-app

Include the bootstrap CDN in index.html.

In contrast to creating the form from scratch, grab this snippet from bootsnipp.

react workspace

This is our beautiful upload form to work with.

Single  React file upload

Let’s start with a simple one, a single file upload.

Capture selected file

Add a change handler in toapp.js pick the file on change.

 <input type="file" name="file" onChange={this.onChangeHandler}/>

Log event.target.files , it is an array of all stored files.  target.files[0]holds the actual file and its details.

onChangeHandler=event=>{

    console.log(event.target.files[0])

}

On saving, create-react-app will instantly refresh the browser.

capture file with reactjs

Store the file in state, and only upload when a user clicks the upload button.

Initially, the selectedFilestate is set to null

constructor(props) {
    super(props);
      this.state = {
        selectedFile: null
      }
   
  }

To pass the file to the state, setselectedFile state to event.target.files[0].

 onChangeHandler=event=>{
    this.setState({
      selectedFile: event.target.files[0],
      loaded: 0,
    })
  }

Check the state variable again with react-devtools to verify.

Again, create-react-app will instantly refresh the browser and you’ll see the result

assign file to state variable in react

Send the files to the server

We have a state of files to upload.

We definitely need an upload button,  upload is handled with onClick event handler.

 <button type="button" class="btn btn-success btn-block" onClick={this.onClickHandler}>Upload</button> 

onClickhandle  will execute onClickHandler which sends a request to the server. The file from a state is appended as a file to FormData.

onClickHandler = () => {
    const data = new FormData() 
    data.append('file', this.state.selectedFile)
}

We’ll use axios to send AJAX requests.

Install and import axios.

import axios from 'axios';

Create form object and create POST request with axios. It needs endpoint URL and form data.

   axios.post("http://localhost:8000/upload", data, { // receive two parameter endpoint url ,form data 
      })
      .then(res => { // then print response status
        console.log(res.statusText)
      })

Here’s final,onClickhandler with axios POST request. It sends POST request to http://localhost:8000/upload and gets response.

onClickHandler = () => {
   const data = new FormData()
   data.append('file', this.state.selectedFile)
   axios.post("http://localhost:8000/upload", data, { 
      // receive two    parameter endpoint url ,form data
  })
.then(res => { // then print response status
    console.log(res.statusText)
 })
}

The file type attached is set as a state and needs to be checked. As a result, it’s a binary file.

attached file to post request with axios

Axios will send a request to the endpoint with a binary file in Form Data.

To receive the uploaded file, implement a backend server. It’ll receive the file sent from front-end.

Create a simple server with Node.

Create server.js file in the root directory

create simple server in nodejs

Install express, multer, and cors.

npm i express multer cors nodemon –save

We’ll use express to create a server, multer to handle files. Cors will be used to enable cross-origin request to this server. Nodemon to monitor the changes and auto-reload, it is optional and you’ll have to restart the server manually in it’s absence.

In,server.js initiate an express instance

var express = require('express');
var app = express();
var multer = require('multer')
var cors = require('cors');

Don’t forget CORS middleware.

app.use(cors())

Create a multer instance and set the destination folder. The code below uses /public folder. You can also assign a new file name upon upload. The code below uses ‘originalfilename’as the file name.

var storage = multer.diskStorage({
      destination: function (req, file, cb) {
      cb(null, 'public')
    },
    filename: function (req, file, cb) {
      cb(null, Date.now() + '-' +file.originalname )
    }
})

Create an upload instance and receive a single file

var upload = multer({ storage: storage }).single('file')

Setup thePOSTroute to upload a file

app.post('/upload',function(req, res) {
     
    upload(req, res, function (err) {
           if (err instanceof multer.MulterError) {
               return res.status(500).json(err)
           } else if (err) {
               return res.status(500).json(err)
           }
      return res.status(200).send(req.file)

    })

});

Start an upload object and handle an error, check formulter error before general errors. Status OK (200) with metadata is sent back to the client on successful upload.

upload response from node server

Make the server listen on port 8000.

app.listen(8000, function() {

    console.log('App running on port 8000');

});

Run nodemon server.js in a terminal to start this server

start node server

Upload a file, you will see the file appear in the public directory.

testing upload with react to nodejs

It’s working, congratulations!

Uploading multiple files in React

It’s time for uploading multiple files at once.

Addmultiplein the input field to accept multiple files in the form.

<input type="file" class="form-control" multiple onChange={this.onChangeHandler}/>

Update andonChangeHandler remove zero indexes, it’s just event.target.files.

onChangeHandler=event=>{
    this.setState({
     selectedFile: event.target.files,
    })
}

Also, update functiononClickHandler to loop through the attached files.

onClickHandler = () => {
   const data = new FormData()
   for(var x = 0; x<this.state.selectedFile.length; x++) {
       data.append('file', this.state.selectedFile[x])
   }

  axios.post("http://localhost:8000/upload", data, { 
      // receive two    parameter endpoint url ,form data
  })

.then(res => { // then print response status
    console.log(res.statusText)
 })

}

In server.js update multer upload instance to accept an array of files.

var upload = multer({ storage: storage }).array('file')

Reload the server and upload multiple files this time.

upload success result

Is it working for you as well? Let us know if it isn’t.

Handling Validation

Until now, nothing has gone wrong but  it doesn’t mean it never will.

Here are situations where this application can crash:

  1. Too many images to upload
  2. Uploading an image with the wrong file extension
  3. Sending an image file that is too large

Client-side validation doesn’t secure the application but can throw errors early to the user and improves the user experience.

#1 There are too many files!

Create a separate function named maxSelectedFile and pass event object.

Use length to check a number of files attached. The code below returns false when a number of files reach 3.


maxSelectFile=(event)=>{
   let files = event.target.files // create file object
       if (files.length > 3) { 
          const msg = 'Only 3 images can be uploaded at a time'
          event.target.value = null // discard selected file
          console.log(msg)
         return false;

     }
   return true;

}

Update onChangeHandler to only set state when the maxSelectFile returns, that is when a number of files are less than 3.

onChangeHandler=event=>{
      var files = event.target.files
      if(this.maxSelectFile(event)){ 
      // if return true allow to setState
         this.setState({
         selectedFile: files
      })
   }
}

The result

max file pick validation result

#2 Uploading an image with the wrong file extension

Create a checkMimeType function and pass an event object

checkMimeType=(event)=>{
  //getting file object
  let files = event.target.files 
  //define message container
  let err = ''
  // list allow mime type
 const types = ['image/png', 'image/jpeg', 'image/gif']
  // loop access array
  for(var x = 0; x<files.length; x++) {
   // compare file type find doesn't matach
       if (types.every(type => files[x].type !== type)) {
       // create error message and assign to container   
       err += files[x].type+' is not a supported format\n';
     }
   };

 if (err !== '') { // if message not same old that mean has error 
      event.target.value = null // discard selected file
      console.log(err)
       return false; 
  }
 return true;

}

Update onChangeHandler again to include checkMimeType.

onChangeHandler=event=>{
      var files = event.target.files
      if(this.maxSelectFile(event) && this.checkMimeType(event))){ 
      // if return true allow to setState
         this.setState({
         selectedFile: files
      })
   }
}

See the output again.

react file upload validation result

#3 Uploading an image that is too large

Create another function checkFileSize to check the file size. Define your limiting size and return false if the uploaded file size is greater.

checkFileSize=(event)=>{
     let files = event.target.files
     let size = 15000 
     let err = ""; 
     for(var x = 0; x<files.length; x++) {
     if (files[x].size > size) {
      err += files[x].type+'is too large, please pick a smaller file\n';
    }
  };
  if (err !== '') {
     event.target.value = null
     console.log(err)
     return false
}

return true;

}

Update onChangeHandler again to handle checkFileSize.

onChangeHandler=event=>{
      var files = event.target.files
      if(this.maxSelectFile(event) && this.checkMimeType(event) &&    this.checkMimeType(event)){ 
      // if return true allow to setState
         this.setState({
         selectedFile: files
      })
   }
}

The output thereafter…

react file upload result

That’s all on client-side validation.

Improve UX with progress bar and Toastify

Letting the user know the happening is a lot better than having them stare at the screen until the upload is finished.

To improve the user experience, we can insert progress bar and a popup message

Progress Bar

Use state variable loaded to update real-time values.
Update the state, add loaded: 0

constructor(props) {
   super(props);
     this.state = {
       selectedFile: null,
       loaded:0
   }
}

The loaded state is changed from progressEvent of the POST request.

axios.post("http://localhost:8000/upload", data, {
       onUploadProgress: ProgressEvent => {
         this.setState({
           loaded: (ProgressEvent.loaded / ProgressEvent.total*100),
       })
   },
})

For progress bar, we use reactstrap.

Install and import progress bar from reactstrap

import {Progress} from 'reactstrap';

Add a progress bar after the file picker.

<div class="form-group">

<Progress max="100" color="success" value={this.state.loaded} >{Math.round(this.state.loaded,2) }%</Progress>

</div>

See the result in action.

react file upload console result

Beautiful, ain’t it?

Display the result message with toastify

Install react-toastify and import the following:

import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

Put the container somewhere

<div class="form-group">
   <ToastContainer />
</div>

Use toast wherever you want to display a message.

First of all, place upload result

.then(res => { 
    toast.success('upload success')
})
.catch(err => { 
    toast.error('upload fail')
})

See the result.

react file upload progress bar result

Also, place validation result.

Update checkMimeType function for validation.

checkMimeType=(event)=>{

    let files = event.target.files
    let err = [] // create empty array
    const types = ['image/png', 'image/jpeg', 'image/gif']
    for(var x = 0; x<files.length; x++) {
        if (types.every(type => files[x].type !== type)) {
        err[x] = files[x].type+' is not a supported format\n';
       // assign message to array
      }
    };
    for(var z = 0; z<err.length; z++) { // loop create toast massage
        event.target.value = null 
        toast.error(err[z])
    }
   return true;
}

You’ve the result

react upload validation result

Also, add toast.warn(msg)

react upload validation result

Include the checkFileSizeand changes from checkMimeType function

checkFileSize=(event)=>{
  let files = event.target.files
  let size = 2000000 
  let err = []; 
  for(var x = 0; x<files.length; x++) {
  if (files[x].size > size) {
   err[x] = files[x].type+'is too large, please pick a smaller file\n';
 }
};
for(var z = 0; z<err.length; z++) {
 toast.error(err[z])
 event.target.value = null
}
return true;
}

Change err variable to array and loop to create toast message from it.

react upload validation result

Our react file upload is working fine, but we can have a lot of improvements like uploading to cloud providers , also use of third-party plugins for other services to improve upload experience are some possible additions.

Before we end of this tutorial,  you can contribute to improve and refactor code from this tutorial send your PR to this repository.

If you loved the tutorial, you might also want to check out Mosh’s Complete React course

And, if this post was helpful in any way, show some support. Please share!

Krissanawat is Nomad Web developer live in Chiangmai passionate on React and Laravel
Tags: ,

17 responses to “React file upload: proper and easy way, with NodeJS!”

  1. Christine Olalo says:

    Hello,

    Would you know how to pass a string as a metadata for the file?

  2. Rajnish says:

    In which file I can define onChangeHandler?

  3. narendar b says:

    Hi I am trying up load image along with other form text fields data but its always defined in node .Either recieving text or Image not both.How to handle if we have both in a form.Will form.append is sufficient do we need to implement completely difffent

  4. When I console.log(event.target.files[0]) it does print the files array, but when I assign it state.selectedFile and console.log it, state.selectedFile doesn’t change, it still null.

  5. Tran Tuyen says:

    Very useful Mosh Hamedani, i would like to ask one thing: How can i store image on database with only original file name ?

  6. Thiago Viotto says:

    Thank you very much. This worked perfeclty! It’s very good tutorial. Congratulations!

  7. Deb says:

    Hi

    I have a requirement which is like

    1. Read a complex excel including its styling like background color, style & calculation.
    2. Need to create another sheet & populate data in that on run time.
    3. User will then edit the newly created sheet after downloading but here the style of the uploaded excel should be retained.

    I have checked everywhere but did not found any. I have been using xlsx module of node in my project for excel manipulation.

    Can you please tell me whether its feasible or not.

  8. Ujjwal Srivastava says:

    Hey Mosh!

    This tutorial was very fulfilling, your style of explaining things is fabulous. I ran the application without using cors middleware and to my surprise files are still being placed in the destined folder. Also, the console get cleared(application re-renders) when I run it without cors. Could you please explain why this is happening?

    Thanks in Advance.

  9. Francisco Vieira Souza says:

    I tried your first option, to upload a single file, but I always get “Current request is not a multipart request” on my Spring Boot server API, I made sure that end point is working testing with Postman.
    Any suggestion?

  10. Ezrqn Kemboi says:

    Thanks so much for this tutorial @Mosh.

  11. winnersingh says:

    I want to create a react app where user can upload the photo and then send it via email. Could you guide me how to do that.

  12. Tony says:

    A great tutorial with no nonsense. Thank you very much!

  13. NIc says:

    the server code did not work for me nomatter what i tried.

    below is the code that worked for me.

    var formidable = require(“formidable”),
    http = require(“http”),
    util = require(“util”),
    fs = require(“fs-extra”);

    http
    .createServer(function(req, res) {
    /* Process the form uploads */
    if (req.url == “/upload” && req.method.toLowerCase() == “post”) {
    var form = new formidable.IncomingForm();
    form.maxFileSize = 200 * 1024 * 1024 * 1024;
    form.parse(req, function(err, fields, files) {
    res.writeHead(200, { “content-type”: “text/plain” });
    res.write(“received upload:\n\n”);
    res.end(util.inspect({ fields: fields, files: files }));
    });

    form.on(“progress”, function(bytesReceived, bytesExpected) {
    var percent_complete = (bytesReceived / bytesExpected) * 100;
    console.log(percent_complete.toFixed(2));
    });

    form.on(“end”, function(fields, files) {
    /* Temporary location of our uploaded file */
    for (let i = 0; i < this.openedFiles.length; i++) {
    var temp_path = this.openedFiles[i].path;
    /* The file name of the uploaded file */
    var file_name = this.openedFiles[i].name;
    /* Location where we want to copy the uploaded file */
    var new_location = "./upload/";

    fs.copy(temp_path, new_location + file_name, function(err) {
    if (err) {
    console.error(err);
    } else {
    console.log("success!");
    }
    });
    }
    });

    return;
    }

    /* Display the file upload form. */
    res.writeHead(200, { "content-type": "text/html" });
    res.end(
    '’ +
    ” +
    ” +
    ” +
    “”
    );
    })
    .listen(8080);

  14. Nic says:

    when i add the progress code to my onClick handler it stops functioning.
    i’m pretty sure i’m not doing anything wrong please assist.

    onUploadProgress: ProgressEvent => {
    this.setState({
    loaded: (ProgressEvent.loaded / ProgressEvent.total*100),
    })

  15. Nic says:

    needed to add corst to the costum server to fix the issue.

    res.setHeader(‘Access-Control-Allow-Origin’, ‘*’);
    res.setHeader(‘Access-Control-Request-Method’, ‘*’);
    res.setHeader(‘Access-Control-Allow-Methods’, ‘OPTIONS, GET’);
    res.setHeader(‘Access-Control-Allow-Headers’, ‘*’);

    was added to the server code.

  16. JACK says:

    Mosh my dear , you have a lot of mistakes in your code.

    If you want to help others , really want , please make sure your code works before posting it.

    Otherwise … you’re just wasting people’s time , and yours.

Leave a Reply

Connect with Me
  • Categories
  • Popular Posts