14 - Upload functionality

We have enough background to finish the migration of the upload & download functionality of the file.io app. In this section, we'll finish the upload functionality, followed by download functionality in the next section. Together, these two sections will up the purpose of this book.

Just to recap, the upload functionality is:

  • Provide a button to choose a file for uploading

  • Call a server API to upload the file

  • Process the results and show it to the user (file name, size, URL, and generated QR code)

A small anime of the vanilla upload functionality is:

Upload functionality in React

In section 8, we've already decomposed the file.io app into a number of small & manageable components. Here is the diagram again:

In the upload functionality, the Header, MainUpload, and Footer components are always visible. If a file has been uploaded, then FileInfo & UploadAnother component will be visible. If a file has not been uploaded yet, then FileUpload will be visible.

Basics

The Upload component code is very straightforward:

Upload.js

import Header from "./Header";
import Footer from "./Footer";
import MainUpload from "./MainUpload";

function Upload() {
  return (
    <div>
      <Header />
      <MainUpload />
      <Footer />
    </div>
  );
}

export default Upload;

The Header, Footer & UploadAnother components are static components. Their code is lifted from vanilla's HTML file. The MainUpload is empty for now. These components also get used in the download functionality. The code of these components is not repeated in the next section.

Headers.js

function Header() {
  return (
    <nav className="navbar navbar-expand-md navbar-dark bg-secondary sticky-top">
      <span className="display-6 text-white">File.io</span>
      <button
        className="navbar-toggler"
        type="button"
        data-toggle="collapse"
        aria-expanded="false"
        aria-label="Toggle navigation"
      >
        <span className="navbar-toggler-icon"></span>
      </button>

      <div className="collapse navbar-collapse align-middle">
        <ul className="navbar-nav">
          <li className="nav-item active text-white tech-sub-heading">
            (React)
          </li>
        </ul>
      </div>
    </nav>
  );
}

export default Header;

Footer.js

import "./Footer.css";

function Footer() {
  return (
    <footer className="footer text-center">
      <div className="container">
        <span className="text-muted">
          Fake file.io service built with React
        </span>
      </div>
    </footer>
  );
}

export default Footer;

Footer.css

.footer {
    position: absolute;
    bottom: 0;
    width: 100%;
    height: 60px;
    line-height: 60px;
    background-color: #f5f5f5;
}

UploadAnother.js

function UploadAnother() {
  return (
    <div>
      <br />
      <a className="mt-2" href="/">Upload another file</a>
    </div>
  );
}

export default UploadAnother;

Let's take a look at the progress so far.

It is good to see that the app is taking shape.

States

The state will be maintained in the MainUpload component. The state will contain the following data:

  • fileUploaded: true/false: Indicates if file has been uploaded or not

  • fileData: This contains information about the uploaded file such as file name, size, and URL.

The QR code doesn't go into state as it gets generated from the file URL.

The state will be initialized to { fileUploaded: false }. This is the only thing that can go in state because other file attributes are not known till the file has been uploaded.

There is some conditional rendering in MainUpload. If fileUploaded is false, then FileUpload will get rendered. If fileUploaded is true, then FileInfo & UploadAnother will get rendered.

This brings us to the important concept of child updating parent's state. In file.io case, we can see that the state is maintained only by the MainUpload component. No other upload related components maintain state. While MainUpload has the state, the file upload functionality is present in the FileUpload component. Once the file gets uploaded, the FileUpload component needs to update the state in MainUpload so that the components gets re-rendered. This is generally achieved through parent callbacks.

The parent component (MainUpload) will create a function to update the state. It'll pass this function to a relevant child in the props object. When the time comes, the child will update the state in the parent by calling the callback provided by the parent.

The following is the code of the MainUpload component. Note the uploadDone function, which is the callback for updating the state in the parent. The uploadDone function combines the file data that comes from child along with setting fileUploaded to true. The job of the child is to send the file data, not worry about the state, fileUploaded attribute, etc. In the next rendering, the parent component renders FileInfo & drops FileUpload. When the parent renders FileInfo, the parent also passes file data to the FileInfo component.

MainUpload.js

import React from "react";
import FileInfo from "./FileInfo";
import FileUpload from "./FileUpload";
import UploadAnother from "./UploadAnother";

function MainUpload() {
  const [uploadData, setUploadData] = React.useState({
    fileUploaded: false,
  });

  function uploadDone(fileData) {
    setUploadData({ ...fileData, fileUploaded: true });
  }

  return (
    <main role="main" className="container mt-5">
      <div className="text-center">
        {uploadData.fileUploaded === false && (
          <FileUpload uploadDoneCb={uploadDone} />
        )}
        {uploadData.fileUploaded === true && <FileInfo fileInfo={uploadData} />}
        {uploadData.fileUploaded === true && <UploadAnother />}
      </div>
    </main>
  );
}

export default MainUpload;

If the code and the explanation doesn't make it clear, the following diagram should help:

The FileUpload component provides a way to upload the file. This component makes use of the useRef hook. The reason was explained in the section 13. We'll not be directly using the default file upload input because it doesn't look the way it looks in the real file.io app. The default file upload input will be hidden. Instead of the file upload input, we'll be using a normal styled button that, on clicking, will simulate a click on the hidden file upload input. We've already seen this in detail towards the end of section 13. The uploaded file is available through the event (like regular JS code event.target.files). The browser fetch API will be used to send the file to the server. Once a response arrives, the parent callback will be used to update the state in parent. The FileUpload component doesn't save anything locally. The parent callback updates state in parent, which, in turn, triggers a re-rendering of parent. This time the rendering will show FileInfo & UploadAnother instead of FileUpload.

FileUpload.js

import React from "react";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";

const MySwal = withReactContent(Swal);

function FileUpload(props) {
  const uploadBtnRef = React.useRef();

  function uploadFile() {
    uploadBtnRef.current.click();
  }

  async function sendFile(e) {
    const file = e.target.files[0];
    const body = new FormData();
    body.set("uploadedFile", file, file.name);
    const resp = await fetch("api/files", {
      method: "PUT",
      body,
    });
    if (resp.status !== 200) {
      MySwal.fire({
        title: "Error!",
        text: "Unable to upload the file",
        icon: "error",
        confirmButtonText: "Ok",
      });
      return;
    }
    const json = await resp.json();
    props.uploadDoneCb({
      fileName: file.name,
      fileSize: file.size,
      fileUrl: json.fileUrl,
    });
  }

  return (
    <div>
      <h3 id="fileHeading">Upload file</h3>
      <br />
      <input
        type="file"
        id="uploadBtn"
        className="uploadBtn"
        name="uploadedFile"
        onChange={sendFile}
        ref={uploadBtnRef}
        hidden
      />
      <input
        type="button"
        id="uploadBtnTemp"
        className="btn btn-primary uploadBtnTemp"
        value="Choose a file"
        onClick={uploadFile}
      />
    </div>
  );
}

export default FileUpload;

As soon as the file gets uploaded, the parent components re-renders. This time it'll show FileInfo & UploadAnother. The file data received from FileUpload gets passed to FileInfo which in turn passes:

  • File name & size to FileDetails component

  • File URL to FileAccess component

FileInfo.js

import FileAccess from "./FileAccess";
import FileDetails from "./FileDetails";

function FileInfo(props) {
  const { fileName, fileSize, fileUrl } = props.fileInfo;

  return (
    <div>
      <h3 className="fileHeading text-success">File Uploaded!</h3>
      <FileDetails fileName={fileName} fileSize={fileSize} />
      <FileAccess fileUrl={fileUrl} />
    </div>
  );
}

export default FileInfo;

The FileDetails component simply displays the received data.

FileDetails.js

import "./FileDetails.css";

function FileDetails(props) {
  function getFileSize(size) {
    const sizeFormatter = new Intl.NumberFormat([], {
      style: "unit",
      unit: "byte",
      notation: "compact",
      unitDisplay: "narrow",
    });
    return sizeFormatter.format(size);
  }

  return (
    <div className="container-sm bg-light p-3 file-details-container">
      <div className="row justify-content-between">
        <div className="col-sm">
          File Name: <br />
          <b>
            <span id="fileName">{props.fileName}</span>
          </b>
        </div>
        <div className="col-sm">
          File Size: <br />
          <b>
            <span id="fileSize">{getFileSize(props.fileSize)}</span>
          </b>
        </div>
      </div>
    </div>
  );
}

export default FileDetails;

FileDetails.css

.file-details-container {
    margin-top: 30px !important;
    width: 30% !important;
}

The FileAccess component uses the file URL to show a QR code. In the vanilla app, we'd used a QR package from CDN. Here, we have already installed QR code as a dependency (from section 9). The QRCode component (provided by that package) can be used as a normal React component.

FileAccess.js

import "./FileAccess.css";
import QRCode from "react-qr-code";

function FileAccess(props) {
  return (
    <div className="container-sm bg-light p-3 file-access-container">
      <div className="row justify-content-between">
        <div className="col-sm">
          File URL: <br />
          <a href={props.fileUrl} id="fileUrl">{props.fileUrl}</a>
        </div>
        <div className="col-sm justify-content-md-left">
          <QRCode value={props.fileUrl} size={128} />
        </div>
      </div>
    </div>
  );
}

export default FileAccess;

FileAccess.css

.file-access-container {
    margin-top: 30px !important;
    width: 30% !important;
}

You may not realize that we've just finished the upload part of the file.io app. Instead of having a big HTML and a big JS file, we now have very small pieces that clubs together to solve the upload puzzle.

Testing it out

To test it out, first start the express server in a different terminal:

learn-react-by-example-file-io-app/react-app/server: npm start

> react-server-app@1.0.0 start
> node --watch server.mjs

(node:64558) ExperimentalWarning: Watch mode is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
File.io vanilla app listening on port 3001

Second, start the webpack server in another terminal:

learn-react-by-example-file-io-app/react-app/client: npm start
Compiled successfully!

You can now view client in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.10.65:3000

Note that the development build is not optimized.
To create a production build, use npm run build.

webpack compiled successfully

Open a browser and go to http://localhost:3000:

This looks exactly like the vanilla app. Now, let's do a round of test:

The upload functionality works as expected. In fact, there is hardly any difference from the vanilla app because we're using the same styling.

--

That's all about the first part of the file.io app: Uploading files. The next section covers the downloading part.

Last updated