DEV Community

Matthew Cullen
Matthew Cullen

Posted on • Edited on

React/Firebase Image Upload Progress Bar

Technology used: Reactjs, Google Firebase, Semantic-ui-react

Alt Text

topics covered

-state
-asynchronous requests
-error handling
-file uploads (images in this tutorial)
-custom components from semantic-ui-react to save time

The goal is to write code that enables a user upload an image file to Google Firebase and display the progress of the upload.
We will be using some custom components from semantic-ui-react to save time.

Step One ~ Setup a Component

Lets start with a simple class component that includes a form with a field for a file upload and a submit button.

import React from "react";
import { 
  Form, 
  Button, 
} from 'semantic-ui-react';

class ExampleForm extends React.Component {
    state = {
      file: null,
      errors: [],
      uploadState: null
    };

    render () {
      return (
        <Form onSubmit={this.handleSubmit}> 
          <Input
              onChange={this.addFile}
              disabled={uploadState === 'uploading'}
              name="file"
              type="file"
           />
         <Button>Post</Button>
        </Form>
      );
    };
}

export default ExampleForm;
Enter fullscreen mode Exit fullscreen mode

We set a variable in state named 'file' set to null and on our inputs onChange we will build out the addFile()function next to store the file a user selects in 'file'.
I also want to disable the file selection when a file is being uploaded so I will add an uploadState value to state and set the disabled property on our input to be true when uploadState is equal to 'uploading'.
We'll build the handleSubmit() function later as well.
the errors array in state is an optional way to keep track of errors. See the uploadFile function below for use.

Step Two ~ addFile()

Lets build out the addFile function

addFile = event => {
  const file = event.target.files[0];
  if (file) {
    this.setState({ file });
  }
};
Enter fullscreen mode Exit fullscreen mode

By passing the event object to our addFile function we can access an array of files attached to it. The image a user selects from clicking our input will be stored in the first index. Lets take this value and store it in our state under the value 'file'.

Step three ~ check file type()

Before we send our file lets make sure our user has actually selected an image file.
To check we have the correct file type lets add a new value to state, an isAuthorized function and import the 'mime-type' library.

state = {
  file: null,
  authorizedFileTypes: ['image/jpeg', 'image/png'],
  errors: []
}
Enter fullscreen mode Exit fullscreen mode
import mime from 'mime-types';

isAuthorized = filename => this.state.authorized.includes(mime.lookup(filename))
Enter fullscreen mode Exit fullscreen mode

isAuthorized will return true if the file type matches one of the strings in our authorizedFileTypes array we added to state.

Step Four ~ uploadFile()

Once we can confirm the file is an image we need a function to upload it to our destination. In this tutorial our destination is Google Firebase. [If you are unsure of how to connect an application to Firebase, I wrote a tutorial on how to do that Here.
I will be importing my unique Firebase config file below, this is where you would import your own unique Firebase config.]
The first step is to import firebase and add a reference of the storage in our Firestore to state. To track our upload lets add a value in state called percentUploaded and set it to 0.

import firebase from 'path in dir to firebase.js';

// state will now look like this
state ={
  storageRef: firebase.storage().ref(), // ADD THIS LINE
  percentUploaded: 0, // ADD THIS LINE TOO
  uploadTask: null // ADD THIS LINE TOO, SEE BELOW
  file: null,
  uploadState: null,
  authorizedFileTypes: ['image/jpeg', 'image/png'],
  errors: []
}
Enter fullscreen mode Exit fullscreen mode

One more thing should be added to state and this is important to getting our progress bar to work later on. We need to add a key that will hold a task or changing value that we will attach a listener to. Lets call it uploadTask and set it to null.

I will show you the completed function then break down what is happening.

// uuidv4() generates a unique id
import uuidv4 from 'uuid/v4';

uploadFile = (file) => {
    // location in storage you want to create/send file to
    const filePath = `trades/images/${uuidv4()}.jpg`;

    this.setState({
      uploadTask: this.state.storageRef.child(filePath).put(file)
    },
      () => {
        this.state.uploadTask.on(
          'state_changed', 
          snap => {
            const percentUploaded = Math.round((snap.bytesTransferred / snap.totalBytes) * 100);
            this.setState({ percentUploaded }); 
        },
          err => {
            console.error(err);
            this.setState({
              errors: this.state.errors.concat(err),
              uploadState: 'error',
              uploadTask: null
            });
          },
          () => {
            this.state.uploadTask.snapshot.ref.getDownloadURL().then(downloadUrl => {
              console.log('this is the image url', downloadUrl);
              this.setState({ uploadState: 'done' })
            })
            .catch(err => {
              console.error(err);
              this.setState({
                errors: this.state.errors.concat(err),
                uploadState: 'error',
                uploadTask: null
              })
            })
          }
        )
      }
    )
}
Enter fullscreen mode Exit fullscreen mode

let's break down what is happening.

this.setState({
      uploadTask: this.state.storageRef.child(filePath).put(file)
    },
      () => {
        this.state.uploadTask.on(
          'state_changed', 
          snap => {
            const percentUploaded = Math.round((snap.bytesTransferred / snap.totalBytes) * 100);
            this.setState({ percentUploaded }); 
        }
Enter fullscreen mode Exit fullscreen mode
  1. We first set the value of uploadTask to the return value of .puts(). This function returns a task which will constantly update until it is complete. The task is our files journey to storage.
  2. Add a listener with .on() to this.state.uploadTask that listens to state changes. Remember this value in state is constantly changing due to .puts()
  3. We get access to a snapshot of our value in uploadTask each time it changes. By accessing some keys associated with this snap shot we can calculate how much of the file has been transferred with Math.round((snap.bytesTransferred / snap.totalBytes) * 100) and set this value to our percentUploaded value in state.
err => {
            console.error(err);
            this.setState({
              errors: this.state.errors.concat(err),
              uploadState: 'error',
              uploadTask: null
            });
          },
          () => {
            this.state.uploadTask.snapshot.ref.getDownloadURL().then(downloadUrl => {
              console.log('this is the image url', downloadUrl);
              this.setState({ uploadState: 'done' })
            })
            .catch(err => {
              console.error(err);
              this.setState({
                errors: this.state.errors.concat(err),
                uploadState: 'error',
                uploadTask: null
              })
            })
          }
        )
Enter fullscreen mode Exit fullscreen mode

the next line checks for errors and if any are found populates the errors array and resets our uploadTask/State. After that we have access to the uploaded images downloadUrl. Now is a good time to set our uploadState to done and clear the file from state. We can then catch any errors again and end our function.

We have now successfully taken an image from our computer and sent it to Google Firestore. But what about the progress bar?

Step Five Progress bar

lets create our own progress bar component and import semantic-ui-react's progress bar into it to customize. We will be passing two values from our first component to the progress bar.

import React from 'react';
import { Progress } from 'semantic-ui-react';

const ProgressBar = ({ uploadState, percentUploaded }) => (
  uploadState === "uploading" && (
    <Progress 
      className="progress__bar"
      percent={percentUploaded}
      progress
      indicating
      size="medium"
      inverted
    />
  )
);

export default ProgressBar;

Enter fullscreen mode Exit fullscreen mode

In our main component lets import this progressBar and add it into our render function. Don't forget to pass uploadState and percentUploaded down as props.

import ProgressBar from 'dir of progress bar';

<ProgressBar 
  uploadState={uploadState} 
  percentUploaded={percentUploaded}
/>
Enter fullscreen mode Exit fullscreen mode

Now as our file uploads we will see our progress bar fill up. The only thing left to do is tie it all together in our handleSubmit function and clean up our component's state and listeners.

Step Six ~ handleSubmit() and componentWillUnmount

handleSubmit = event => {
   uploadFile(this.state.file);
  };

componentWillUnmount() {
    if (this.state.uploadTask !== null) {
      this.state.uploadTask.cancel();
      this.setState({ uploadTask: null });
    }
  }
Enter fullscreen mode Exit fullscreen mode

There you have it, an image upload and a % upload tracker. I hope this helps anyone working with Firebase and React.

Bonus error handling

lets make two functions to handle any errors we may encounter.
First give the our file input element a className equal to calling a new function we will setup called handleInputErrors. Pass this function our errors array from state and a 'keyword' to look for.

<Input
   className={this.handleInputError(errors,'file')}
   onChange={this.addFile}
   disabled={uploadState === 'uploading'}
   fluid
   label="File types: jpg, png"
   name="file"
   type="file"
/>
Enter fullscreen mode Exit fullscreen mode
handleInputError = (errors, inputName) => {
    return errors.some(error => 
      error.message.toLowerCase().includes(inputName)
      )
        ? "error"
        : ""
  }

  displayErrors = errors => errors.map((error, i) => <p key={i}>{error.message}</p>);
Enter fullscreen mode Exit fullscreen mode

The first function, handleInputError will check if we have an error in our array dealing with 'file' and if so our className will become 'error'. We can style the error class red so that when the className becomes error it will reflect that state to our user. Our last function displayErrors will just map over our errors array and display the message associated with each.

Top comments (0)