Learn React in a Day
  • LEARN REACT IN A DAY
  • Disclaimer & Copywright
  • Audience
  • Reviews for this book
  • PART 1 - INTRODUCTION
    • Part 1 - Contents
    • 1 - What is React?
    • 2 - The popularity of React
    • 3 - Teaching style of this book
    • 4 - File.io service in Vanilla JS
  • PART 2 - THE BASICS
    • Part 2 - Contents
    • 5 - Setting up React development environment
    • 6 - Structure of a React app
    • 7 - JSX
    • 8 - (Re)Designing file.io for React
  • PART 3 - THE PREPARATION
    • Part 3 - Contents
    • 9 - Setting up
    • 10 - Routing
    • 11 - Props
    • 12 - States
    • 13 - Effect & Ref
  • PART 4 - THE MIGRATION
    • Part 4 - Contents
    • 14 - Upload functionality
    • 15 - Download functionality
    • 16 - Next steps
Powered by GitBook
On this page
  • Download functionality in React
  • Testing it out
  1. PART 4 - THE MIGRATION

15 - Download functionality

Previous14 - Upload functionalityNext16 - Next steps

Last updated 1 year ago

In the previous section (section ), we've finished the upload functionality of the file.io app. That covers one half of the app. In this section, we'll finish the download functionality, the other half of the app.

Just to recap, the download functionality is:

  • On page load, make an API call to the server using the file ID from the URL

  • If the server returns success, show file details and a download button

  • If the server returns 404, show the file not exists error

  • When the download button is pressed, get the file from the server (server removes it from the storage as soon as the response gets sent)

A small anime of the vanilla download functionality is:

Download functionality in React

Just like upload, the Header, MainDownload, and Footer are always visible. If server API results in failure, then FileError component will be displayed. If server API succeeds (meaning that file exists), then FileDetails & FileDownload components will be displayed. The FileDetails component is the same one that we've seen in upload functionality. This is the power of creating small, manageable, and reusable components.

The Download component code is very straightforward:

Download.js

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

function Download(props) {
  return (
    <div>
      <Header />
      <MainDownload fileId={props.fileId} />
      <Footer />
    </div>
  );
}

export default Download;

States

Just like the upload case, where the state was maintained in the MainUpload component, in the download case, the state is maintained in the MainDownload component. The state will contain the following data:

  • fileFound: true/false: Indicates if file has been found or not

  • fileData: This contains information about the file such as file name & size

The state will be initialized to { fileFound: false }. This is the only thing that can go in state because other file attributes are not known till file has been found. Unless the server API confirms existence of the file, the file is considered to be not available. In other words, the default behavior is file no available.

This leads to some conditional rendering in the MainDownload component. If fileFound is false, then FileError will get rendered. If fileFound is true, then FileDetails & FileDownload gets rendered. In all cases, UploadAnother will get rendered. The UploadAnother component was conditionally rendered in the upload case, but is always shown in the download case.

The download functionality also needs to implement the concept of child updating parent's state. In the upload case, we saw that the state was maintained only by the MainUpload component. No other upload related components maintained any state. While MainUpload had the state, the file upload functionality was present in the FileUpload component. The design is the same with the download case. Only MainDownload maintains the state. No other download related components maintain any kind of state. The FileDownload component is responsible for getting the file downloaded. As soon as the button gets clicked, an API will be called to get the file. The server removes the file as soon as it gets served. Therefore, the FileDownload component needs to indicate to the parent that the file no longer exists as soon as it gets downloaded. This gets achieved through parent callbacks (just like we did it for the upload case).

The following is the code of the MainDownload component. As soon as the component gets rendered, useEffect hook gets invoked to fetch the file details from the server. This will be configured to run exactly once (using [] in dependency list). The result of the API call sets the state which trigger re-rendering of the MainDownload component. If the file has been found, the received file data gets passed to FileDetails & FileDownload.

MainDownload.js

import React from "react";
import FileError from "./FileError";
import FileDetails from "./FileDetails";
import FileDownload from "./FileDownload";
import UploadAnother from "./UploadAnother";

function MainDownload(props) {
  const [fileData, setFileData] = React.useState({
    fileFound: false,
    fileName: "",
    fileSize: "",
  });

  function clearFileData() {
    setFileData({ fileFound: false });
  }

  React.useEffect(() => {
    fetch(`api/files/${props.fileId}/meta`)
      .then((resp) => resp.json())
      .then((json) => setFileData({ ...json, fileFound: true }))
      .catch(() => {
        setFileData({ fileFound: false });
      });
  }, [props.fileId]);

  return (
    <main role="main" className="container mt-5">
      <div className="text-center">
        <h3 id="fileHeading">Download file</h3>
        <br />
        {fileData.fileFound === false && <FileError />}
        {fileData.fileFound && (
          <FileDetails
            fileName={fileData.fileName}
            fileSize={fileData.fileSize}
          />
        )}
        {fileData.fileFound && (
          <FileDownload
            fileId={props.fileId}
            fileName={fileData.fileName}
            clearFileCb={clearFileData}
          />
        )}
        <UploadAnother />
      </div>
    </main>
  );
}

export default MainDownload;

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

The FileDownload component is responsible for getting the file to the user. As soon the file gets to the browser, the server deletes it. The FileDownload component immediately uses parent callback to indicate that the file no longer exists. The parent gets re-rendered and enables the FileError component in this render. Just like FileUpload offering functionality through a visible and a hidden button with click simulation, the FileDownload also offers functionality through a visible and a hidden button with click simulation. A regular styled button is shown to the user. But the download functionality is handled by HTML's built-in <a download> feature. Using React's useRef, a reference for hidden <a download> is created. This reference is clicked when the styled button gets clicked. To enable storing of the file on the disk, the received file needs to be converted to a data URL that goes into the <a download> element. Once the data URL is set, then only <a download> gets clicked programmatically. (this is a standard JS feature).

FileDownload.js

import { useEffect, useRef, useState } from "react";
import "./FileDownload.css";

function FileDownload(props) {
  const dataLink = useRef();
  const [fileData, setFileData] = useState("");

  useEffect(() => {
    if (fileData) {
      dataLink.current.click();
      setTimeout(() => props.clearFileCb(), 1000);
    }
  }, [fileData]);

  async function buttonClick() {
    const resp = await fetch(`api/files/${props.fileId}`);
    const respBlob = await resp.blob();
    setFileData(URL.createObjectURL(respBlob));
  }

  return (
    <div className="container-sm download-container bg-light p-3">
      <a download={props.fileName} href={fileData} ref={dataLink}></a>
      <input
        type="button"
        className="btn btn-primary"
        value="Download file"
        onClick={buttonClick}
      />
    </div>
  );
}

export default FileDownload;

The only remaining component in the download functionality is the FileError component. This is a static component that shows an error.

FileError.js

import "./FileError.css";

function FileError() {
  return (
    <div className="container-sm file-error-container p-2 bg-danger text-white">
      File doesn't exist
    </div>
  );
}

export default FileError;

FileError.css

.file-error-container {
    width: 20% !important;
}

Again, you may not realize that we've just finished the download 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 download puzzle. Combining this with the upload puzzle, the complete puzzle is solved now. Congratulations!

Testing it out

To test it out, first start the 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. First, we'll upload a file, then download it. This is our final end to end testing.

The upload & download functionality works as expected. In fact, there is hardly any difference from the vanilla app. We've finished migrating vanilla app to React!

--

That's all about it. In the next and the last section of this book, we'll go through some possible next steps for you.

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

Note: Header, Footer, UploadAnother, and FileDetails components have been reused in download functionality. Their code is not shown in this section. You can see their code in section .

The Header & Footer are the same components that we've used in the upload functionality. The fileId parameter is coming from the React router param extraction. To get a quick recap, you can visit section . The fileId parameter gets forwarded to the MainDownload component.

8
14
10
14