Categories
JavaScript

Introduction to JavaScript Data Types

JavaScript, like any other programming language, has its own data structures and types. JavaScript has a few data types that we have to know about in order to build programs with it. Different pieces of data can be put together to build more complex data structures.

JavaScript is a loosely typed, or dynamically typed, language. This means that a variable that’s declared with one type can be converted to another type without explicitly converting the data to another type. Variables can also contain any type at any time, depending on what’s assigned. For example, if we write the following code:

let x = 1;      
x = 'bar';  
x = true;

In the first line x is a number, but in the second line, the same variable x has been reassigned into a string. In the last line, it has been again been reassigned, this time to a Boolean.

JavaScript has multiple data types. There are seven primitive data types and an object type. The seven primitive types are Boolean, null, undefined, number, BigInt, string, and symbol.

All of JavaScript’s primitive types are immutable, which means that they can’t be changed. The primitive types contain values that are fixed once they’re defined. The Boolean type is either true or false and represents logical entities. The null type only has one value, which is null. The null value means that it refers to some nonexistent or invalid object or address. The undefined type is unique to JavaScript. It means that a variable hasn’t been assigned any value.


Numbers

There are two number types in JavaScript, which are number and BigInt. The number type is a double-precision 64-bit number that can have values between -2 to the 53rd power minus 1 and 2 to the 53rd power minus 1. There’s no specific type for integers. All numbers are floating point numbers. There are also three symbolic values: Infinity , -Infinity and NaN.

The largest and smallest available values for a number are Infinity and -Infinity respectively. We can also use the constants Number.MAX_VALUE or Number.MIN_VALUE to represent the largest and smallest numbers. We can use the Number.isSafeInteger() function to check whether a number is in the range of numbers available that are allowed in JavaScript. There is also the constants Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_NUMBER to check if the number you specify in the safe range. Anything outside the range isn’t safe and will be a double-precision floating-point of the value. The number 0 has two representations in JavaScript: There’s +0 and -0, and 0 is an alias for +0. It will be noticed if you try to divide a number by 0:

1/+0 // Infinity  
1/-0 // -Infinity

Sometimes numbers can represent Boolean values with bitwise operators to operate them as Boolean, but this is bad practice since JavaScript already has Boolean types, so using numbers to represent Boolean will be unclear to people reading the code. That’s because numbers can represent numbers, or they can represent Booleans if someone chooses to use them that way.

In JavaScript, there is the BigInt type to store numbers that are beyond safe integer range. A BigInt number can be created by adding an n character to the end of a number. With BigInt, we can make calculations that have results beyond the safe range of normal numbers. For example, we can write the following expressions and still get the numbers we expect:

const x = 2n ** 55n;  
const y = x + 1n;

For x we get 36028797018963968n and for y we get 36028797018963969n , which is what we expect. BigInts can use the same arithmetic operations as numbers like +, *, -, ** and %. A BigInt behaves like a number when converted to a Boolean with functions, keywords, or operators like Boolean, if , || , &&, !. BigInts cannot be operated in the same expressions as numbers. If we try that, we will get a TypeError.


Strings

Strings are used to represent textual data. Each element of the string has its own position in the string. It’s zero-indexed, so the position of the first character of a string is 0. The length property of the string has the total number of characters of the string.

JavaScript strings are immutable. We cannot modify a string that has been created, but we can still create a new string that contains the originally defined string. We can extract substrings from a string with the substr() function and use the concat() function to concatenate two strings.

We should only present text data with strings. If there are more complex structures needed for your data structure, then they shouldn’t be represented with a string. Instead, they should be objects. This is because it’s easy to make mistakes with strings since we can put in the characters we want. Therefore, mistakes are made easily.


Symbols

Symbols are new to ES2015. It is a unique and immutable identifier. Once you have created it, it cannot be copied. Every time you create a new symbol, it’s a unique one. Symbols are mainly used for unique identifiers in an object. That’s a symbol’s only purpose.

There are some static properties and methods of its own that expose the global symbol registry. It is like a built-in object, but it doesn’t have a constructor, so we can’t write new Symbol to construct a symbol object with the new keyword.

To create new symbols, we can write:

const fooSymbol = Symbol('foo')

Note that each time we call the Symbol function, we get a new symbol, so if we write

Symbol('sym') === Symbol('sym')

the expression above would be false.


Objects

Object is a reference data type, which means it can be referenced by an identifier that points to the location of the object in memory. In memory, the object’s value is stored, and, with the identifier, we can access the value. Object has properties, which are key-value pairs with the values being able to contain the data with primitive types or other objects. That means we can use object to build complex data structures. The key is an identifier for the values of a property, which can be stored as a string or symbol. There are two types of properties that have certain attributes in an object. Objects have data properties and accessor properties.

A JavaScript object has the following data properties:

  • [[Value]] — This can be of any type. It has the value retrieved by a getter of the property. Defaults to undefined.
  • [[Writable]] — This is a Boolean value. If it’s false, then [[Value]] can’t be changed. Defaults to false.
  • [[Enumerable]] — This is a Boolean value. If it’s true, then it can be iterated over by the for...in loop, which is used to iterate over the properties of an object. Defaults to false.
  • [[Configurable]] — This is a Boolean value. If it’s true, then the property can be deleted or changed to an accessor property, and all attributes can be changed. Otherwise, the property can’t be deleted or changed to an accessor property, and attributes other than [[Value]] and [[Writable]] can’t be changed. Defaults to false.

A JavaScript object has the following accessor properties:

  • [[Get]] — This is either a function or it’s undefined. This may contain a function that is used to retrieve a property value whenever a property is being retrieved. Defaults to undefined.
  • [[Set]] — This is either a function or it’s undefined. This lets us set the assigned value to an object’s property whenever an object’s property is attempted to be changed. Defaults to undefined.
  • [[Enumerable]] — This is a Boolean value that defaults to false. If it’s true, then the property will be included when we loop through the properties with the for...in loop.
  • [[Configurable]] — This is a Boolean value that defaults to false. If it’s false, then we can’t delete the property and can’t make changes to it.

JavaScript has a Date object built into the standard library, so we can use it to manipulate dates.

Array are also objects. Array can store a list of data with integer indexes to indicate its position. The first index of JavaScript arrays is 0. There’s also a length property to get the size of the array. The Array object has many convenient methods to manipulate arrays, like the push method to add items to the end of the array and the indexOf method to find the index of the first occurrence of a given value. ATypedArray object is an object that lets us see an array-like view of a binary data buffer.

Since ES2015, the following typed array objects are available:

  • Int8Array, value ranges from -128 to 127
  • Uint8Array, value ranges from 0 to 255
  • Uint8ClampedArray, value ranges from 0 to 255
  • Int16Array, value ranges from -32768 to 32767
  • Uint16Array, value ranges from 0 to 65535
  • Int32Array, value ranges from -2147483648 to 2147483647
  • Unit32Array, value ranges from 0 to 4294967295
  • Floar32Array, value ranges from -1.2 times 10 to the 38 to 3.4times 10 to the 38
  • Float64Array, value ranges from 5.0 times 10 to the 324 to 1.8 times 10 to the 308
  • BigInt64Array, value ranges from -2 to the 63 to 2 to the 63 minus 1
  • BigUint64Array, value ranges from 0 to 2 to the 64 minus 1

Since ES2015, we have new iterable object types. They are Map, Set, WeakMap, WeakSet. Set and WeakSet represent sets of objects, and Map and WeakMap represent objects with a list of key-value pairs. Map keys can be iterated over, butWeakMap’s keys cannot be.


JSON

JSON is a data serialization format that resembles JavaScript objects. They can contain plain data, which means that functions and other kinds of dynamic code aren’t valid in JSON. JSON is frequently used for sending and receiving data for HTTP requests.


The Typeof Operator

With the typeof operator, we can determine the type of a variable or a given value.


JavaScript data types are for classifying data stored in memory into different types. We have numbers and BigInts to store numbers large and small. Also, we have strings to store data. To store more complex data, we can use objects to store things like lists in array objects and maps for key-value pairs. Objects themselves also consist of key-value pairs. In recent versions of JavaScript, we also have Typed Arrays to store binary data with different ranges of values. To transmit and receive data, we can use JSON. Finally, we can use the typeof operator to determine the data type of your variable or value.

Categories
React

How to Build a Simple Video Converter with Node.js

Videos are an incredibly popular medium for sharing information, but in the past, converting videos has always been a problem for people. With FFMPEG, this can now be solved easily.

What is FFMPEG

FFMPEG is a command line video and audio processing program that has many capabilities. It also supports multiple formats for video conversion. The capabilities of FFMPEG include: getting video and audio metadata, changing the resolution of videos, changing audio quality, compressing videos, removing audio from videos, remove video from audio, extract frames from video, change video aspect ratio, and much more.

Developers have done the hard work for us by creating a Node.js wrapper for FFMPEG. The package is called fluent-ffmpeg. It is located at https://github.com/fluent-ffmpeg/node-fluent-ffmpeg. This package allows us to run FFMPEG commands by calling the built-in functions.

In this article, we will build a video converter to change the source video into the format of the user’s choice. We will use the fluent-ffmpeg package for running the conversions. Because the jobs are long-running, we will also create a job queue so that it will run in the background.

We will use Express for the back end and React for the front end.

Back End

To get started, create a project directory and a backend folder inside it. In the backend folder, run npx express-generator to generate the files for the Express framework.

Next run npm i in the backend folder to download the packages in package.json.

Then we have to install our own packages. We need Babel to use import in our app. Also, we will use the Bull package for background jobs, CORS package for cross domain requests with the front end, fluent-ffmpeg for converting videos, Multer for file upload, Dotenv for managing environment variables, Sequelize for ORM, and SQLite3 for or database.

Run npm i @babel/cli @babel/core @babel/node @babel/preset-env bull cors dotenv fluent-ffmpeg multer sequelize sqlite3 to install all the packages.

Next add the .babelrc file to the backend folder:

{
    "presets": [
        "@babel/preset-env"
    ]
}

This enables the latest JavaScript features.

In the scripts section of package.json, replace the existing code with:

"start": "nodemon --exec npm run babel-node --  ./bin/www",
"babel-node": "babel-node"

This runs our program with Babel instead of the regular Node runtime.

Next, we create the Sequelize code by running npx sequelize-cli init which creates a config.json file.

Then in config.json, we replace the existing code with:

{
  "development": {
    "dialect": "sqlite",
    "storage": "development.db"
  },
  "test": {
    "dialect": "sqlite",
    "storage": "test.db"
  },
  "production": {
    "dialect": "sqlite",
    "storage": "production.db"
  }
}

Next we need to create our model and migration. We run:

npx sequelize-cli --name VideoConversion --attributes filePath:string,convertedFilePath:string,outputFormat:string,status:enum

To create the model and migration for the VideoConversions table.

In the newly created migration file, replace the existing code with:

"use strict";
module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable("VideoConversions", {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      filePath: {
        type: Sequelize.STRING
      },
      convertedFilePath: {
        type: Sequelize.STRING
      },
      outputFormat: {
        type: Sequelize.STRING
      },
      status: {
        type: Sequelize.ENUM,
        values: ["pending", "done", "cancelled"],
        defaultValue: "pending"
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable("VideoConversions");
  }
};

This adds the constants for our enum.

In models/videoconversion.js, replace the existing code with:

"use strict";
module.exports = (sequelize, DataTypes) => {
  const VideoConversion = sequelize.define(
    "VideoConversion",
    {
      filePath: DataTypes.STRING,
      convertedFilePath: DataTypes.STRING,
      outputFormat: DataTypes.STRING,
      status: {
        type: DataTypes.ENUM("pending", "done", "cancelled"),
        defaultValue: "pending"
      }
    },
    {}
  );
  VideoConversion.associate = function(models) {
    // associations can be defined here
  };
  return VideoConversion;
};

This add the enum constants to the model.

Next run npx sequelize-init db:migrate to create our database.

Then create a files folder in the backend directory for storing the files.

Next we create our video processing job queue. Create a queues folder and inside it, create videoQueue.js file and add:

const Queue = require("bull");
const videoQueue = new Queue("video transcoding");
const models = require("../models");
var ffmpeg = require("fluent-ffmpeg");
const fs = require("fs");

const convertVideo = (path, format) => {
  const fileName = path.replace(/.[^/.]+$/, "");
  const convertedFilePath = `${fileName}_${+new Date()}.${format}`;
  return new Promise((resolve, reject) => {
    ffmpeg(`${__dirname}/../files/${path}`)
      .setFfmpegPath(process.env.FFMPEG_PATH)
      .setFfprobePath(process.env.FFPROBE_PATH)
      .toFormat(format)
      .on("start", commandLine => {
        console.log(`Spawned Ffmpeg with command: ${commandLine}`);
      })
      .on("error", (err, stdout, stderr) => {
        console.log(err, stdout, stderr);
        reject(err);
      })
      .on("end", (stdout, stderr) => {
        console.log(stdout, stderr);
        resolve({ convertedFilePath });
      })
      .saveToFile(`${__dirname}/../files/${convertedFilePath}`);
  });
};

videoQueue.process(async job => {
  const { id, path, outputFormat } = job.data;
  try {
    const conversions = await models.VideoConversion.findAll({ where: { id } });
    const conv = conversions[0];
    if (conv.status == "cancelled") {
      return Promise.resolve();
    }
    const pathObj = await convertVideo(path, outputFormat);
    const convertedFilePath = pathObj.convertedFilePath;
    const conversion = await models.VideoConversion.update(
      { convertedFilePath, status: "done" },
      {
        where: { id }
      }
    );
    Promise.resolve(conversion);
  } catch (error) {
    Promise.reject(error);
  }
});

export { videoQueue };

In the convertVideo function, we use fluent-ffmpeg to get the video file, then set the FFMPEG and FFProbe paths from the environment variables. Then we call toFormat to convert it to the format that the user wants. We add a log statement in the start, error, and end handlers to see the outputs and resolve our promise on the end event. When the conversion is complete, we save it to a new file.

videoQueue is a Bull queue that processes jobs in the background sequentially. Redis is required to run the queue, we will need a Ubuntu Linux installation. We run the following commands in Ubuntu to install and run Redis:

$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo apt-get install redis-server
$ redis-server

In the callback of the videoQueue.process function, we call the convertVideo function and update the path of the converted file and the status of the given job when the job is done.

Next we create our routes. Create a conversions.js file in the routes folder and add:

var express = require("express");
var router = express.Router();
const models = require("../models");
var multer = require("multer");
const fs = require("fs").promises;
const path = require("path");
import { videoQueue } from "../queues/videoQueue";
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "./files");
  },
  filename: (req, file, cb) => {
    cb(null, `${+new Date()}_${file.originalname}`);
  }
});
const upload = multer({ storage });

router.get("/", async (req, res, next) => {
  const conversions = await models.VideoConversion.findAll();
  res.json(conversions);
});

router.post("/", upload.single("video"), async (req, res, next) => {
  const data = { ...req.body, filePath: req.file.path };
  const conversion = await models.VideoConversion.create(data);
  res.json(conversion);
});

router.delete("/:id", async (req, res, next) => {
  const id = req.params.id;
  const conversions = await models.VideoConversion.findAll({ where: { id } });
  const conversion = conversions[0];
  try {
    await fs.unlink(`${__dirname}/../${conversion.filePath}`);
    if (conversion.convertedFilePath) {
      await fs.unlink(`${__dirname}/../files/${conversion.convertedFilePath}`);
    }
  } catch (error) {
  } finally {
    await models.VideoConversion.destroy({ where: { id } });
    res.json({});
  }
});

router.put("/cancel/:id", async (req, res, next) => {
  const id = req.params.id;
  const conversion = await models.VideoConversion.update(
    { status: "cancelled" },
    {
      where: { id }
    }
  );
  res.json(conversion);
});

router.get("/start/:id", async (req, res, next) => {
  const id = req.params.id;
  const conversions = await models.VideoConversion.findAll({ where: { id } });
  const conversion = conversions[0];
  const outputFormat = conversion.outputFormat;
  const filePath = path.basename(conversion.filePath);
  await videoQueue.add({ id, path: filePath, outputFormat });
  res.json({});
});

module.exports = router;

In the POST / route, we accept the file upload with the Multer package. We add the job and save the file to the files folder that we created before. We save it with the file’s original name in the filename function in the object we passed into the diskStorage function and specified the file be saved in the files folder in the destination function.

The GET route / route gets the jobs added. DELETE / deletes the job with the given ID along with the original and converted file of the job. PUT /cancel/:id route sets status to cancelled .

The GET /start/:id route adds the job with the given ID to the queue we created earlier.

In app.js, we replace the existing code with:

require("dotenv").config();
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var cors = require("cors");

var indexRouter = require("./routes/index");
var conversionsRouter = require("./routes/conversions");

var app = express();

// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");

app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(express.static(path.join(__dirname, "files")));
app.use(cors());

app.use("/", indexRouter);
app.use("/conversions", conversionsRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};

// render the error page
  res.status(err.status || 500);
  res.render("error");
});

module.exports = app;

This adds the CORS add-on to enable cross domain communication, exposes the files folder to the public, and exposes the conversions routes we created earlier to the public.

To add the environment variables, create an .env file in the backend folder and add:

FFMPEG_PATH='c:ffmpegbinffmpeg.exe'
FFPROBE_PATH='c:ffmpegbinffprobe.exe'

Front End

With back end done, we can move on to the front end. In the project’s root folder, run npx create-react-app frontend to create the front end files.

Next we install some packages. We need Axios for making HTTP requests, Formik for form value handling, MobX for state management, React Router for routing URLs to our pages, and Bootstrap for styling.

Run npm i axios bootstrap formik mobx mobx-react react-bootstrap react-router-dom to install the packages.

Next we replace the existing code in App.js with:

import React from "react";
import { Router, Route } from "react-router-dom";
import "./App.css";
import { createBrowserHistory as createHistory } from "history";
import HomePage from "./HomePage";
import { ConversionsStore } from "./store";
import TopBar from "./TopBar";
const conversionsStore = new ConversionsStore();
const history = createHistory();

function App() {
  return (
    <div className="App">
      <TopBar />
      <Router history={history}>
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} conversionsStore={conversionsStore} />
          )}
        />
      </Router>
    </div>
  );
}

export default App;

We add the top bar and the routes in this file.

In App.css, we replace the existing code with:

.page {
  padding: 20px;
}

.button {
  margin-right: 10px;
}

Next create HomePage.js in the src folder and add:

import React from "react";
import Table from "react-bootstrap/Table";
import Button from "react-bootstrap/Button";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import { observer } from "mobx-react";
import {
  getJobs,
  addJob,
  deleteJob,
  cancel,
  startJob,
  APIURL
} from "./request";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";

function HomePage({ conversionsStore }) {
  const fileRef = React.createRef();
  const [file, setFile] = React.useState(null);
  const [fileName, setFileName] = React.useState("");
  const [initialized, setInitialized] = React.useState(false);

  const onChange = event => {
    setFile(event.target.files[0]);
    setFileName(event.target.files[0].name);
  };

  const openFileDialog = () => {
    fileRef.current.click();
  };

  const handleSubmit = async evt => {
    if (!file) {
      return;
    }
    let bodyFormData = new FormData();
    bodyFormData.set("outputFormat", evt.outputFormat);
    bodyFormData.append("video", file);
    await addJob(bodyFormData);
    getConversionJobs();
  };

  const getConversionJobs = async () => {
    const response = await getJobs();
    conversionsStore.setConversions(response.data);
  };

  const deleteConversionJob = async id => {
    await deleteJob(id);
    getConversionJobs();
  };

  const cancelConversionJob = async id => {
    await cancel(id);
    getConversionJobs();
  };

  const startConversionJob = async id => {
    await startJob(id);
    getConversionJobs();
  };

  React.useEffect(() => {
    if (!initialized) {
      getConversionJobs();
      setInitialized(true);
    }
  });

  return (
    <div className="page">
      <h1 className="text-center">Convert Video</h1>
      <Formik onSubmit={handleSubmit} initialValues={{ outputFormat: "mp4" }}>
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group
                as={Col}
                md="12"
                controlId="outputFormat"
                defaultValue="mp4"
              >
                <Form.Label>Output Format</Form.Label>
                <Form.Control
                  as="select"
                  value={values.outputFormat || "mp4"}
                  onChange={handleChange}
                  isInvalid={touched.outputFormat && errors.outputFormat}
                >
                  <option value="mov">mov</option>
                  <option value="webm">webm</option>
                  <option value="mp4">mp4</option>
                  <option value="mpeg">mpeg</option>
                  <option value="3gp">3gp</option>
                </Form.Control>
                <Form.Control.Feedback type="invalid">
                  {errors.outputFormat}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>

            <Form.Row>
              <Form.Group as={Col} md="12" controlId="video">
                <input
                  type="file"
                  style={{ display: "none" }}
                  ref={fileRef}
                  onChange={onChange}
                  name="video"
                />
                <ButtonToolbar>
                  <Button
                    className="button"
                    onClick={openFileDialog}
                    type="button"
                  >
                    Upload
                  </Button>
                  <span>{fileName}</span>
                </ButtonToolbar>
              </Form.Group>
            </Form.Row>

            <Button type="submit">Add Job</Button>
          </Form>
        )}
      </Formik>

      <br />
      <Table>
        <thead>
          <tr>
            <th>File Name</th>
            <th>Converted File</th>
            <th>Output Format</th>
            <th>Status</th>
            <th>Start</th>
            <th>Cancel</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          {conversionsStore.conversions.map(c => {
            return (
              <tr>
                <td>{c.filePath}</td>
                <td>{c.status}</td>
                <td>{c.outputFormat}</td>
                <td>
                  {c.convertedFilePath ? (
                    <a href={`${APIURL}/${c.convertedFilePath}`}>Open</a>
                  ) : (
                    "Not Available"
                  )}
                </td>
                <td>
                  <Button
                    className="button"
                    type="button"
                    onClick={startConversionJob.bind(this, c.id)}
                  >
                    Start
                  </Button>
                </td>
                <td>
                  <Button
                    className="button"
                    type="button"
                    onClick={cancelConversionJob.bind(this, c.id)}
                  >
                    Cancel
                  </Button>
                </td>
                <td>
                  <Button
                    className="button"
                    type="button"
                    onClick={deleteConversionJob.bind(this, c.id)}
                  >
                    Delete
                  </Button>
                </td>
              </tr>
            );
          })}
        </tbody>
      </Table>
    </div>
  );
}

export default observer(HomePage);

This is the home page of our app. We have a dropdown for selecting the format of the file, an upload button for select the file for conversion, and a table for displaying the video conversion jobs with the status and file names of the source and converted files.

We also have buttons to start, cancel, and delete each job.

To add file upload, we have a hidden file input and in the onChange handler of the file input. The Upload button’s onClick handler will click the file input to open the upload file dialog.

We get latest jobs by calling getConversionJobs when we first load the page, and also when we start, cancel, and delete jobs. The job data is stored in the MobX store that we will create later. We wrap observer in our HomePage in the last line to always get the latest values from the store.

Next create request.js and the src folder and add:

const axios = require("axios");
export const APIURL = "http://localhost:3000";

export const getJobs = () => axios.get(`${APIURL}/conversions`);

export const addJob = data =>
  axios({
    method: "post",
    url: `${APIURL}/conversions`,
    data,
    config: { headers: { "Content-Type": "multipart/form-data" } }
  });

export const cancel = id => axios.put(`${APIURL}/conversions/cancel/${id}`, {});

export const deleteJob = id =>
  axios.delete(`${APIURL}/conversions/${id}`);

export const startJob = id => axios.get(`${APIURL}/conversions/start/${id}`);

The HTTP requests that we make to the back end are all here. They were used on the HomePage .

Next create the MobX store by creating the store.js file in the src folder. In there add:

import { observable, action, decorate } from "mobx";

class ConversionsStore {
  conversions = [];

  setConversions(conversions) {
    this.conversions = conversions;
  }
}

ConversionsStore = decorate(ConversionsStore, {
  conversions: observable,
  setConversions: action
});

export { ConversionsStore };

This is a simple store that persists the contacts, and the conversions array is where we store the contacts for the whole app. The setConversions function lets us set contacts from any component where we pass in this store object to.

Next create TopBar.js in the src folder and add:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";

function TopBar() {
  return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">Video Converter</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/">Home</Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}

export default TopBar;

This contains the React Bootstrap Navbar to show a top bar with a link to the home page and the name of the app.

In index.html, we replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Video Converter</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

This adds the Bootstrap CSS and changes the title.

After writing all that code, we can run our app. Before running anything, install nodemon by running npm i -g nodemon so that we don’t have to restart the back end ourselves when files change.

Then run back end by running npm start in the backend folder and npm start in the frontend folder, then choose ‘yes’ if you’re asked to run it from a different port.

Categories
React

How to Add Authenticated Routes to Your React App

Authenticated Reacts routes are easy to make with higher order components.

Authenticated Reacts routes are easy to make with higher order components. Higher order components are components that takes components as props and return render a new component.

For example, to create an authenticated route, we can write:

import React from "react";
import { Redirect } from "react-router-dom";

function RequireAuth({ Component }) {
  if (!localStorage.getItem("token")) {
    return <Redirect to="/" />;
  }
  return <Component />;
}

export default RequireAuth;

RequireAuth is a component that takes a Component prop. Component is a prop that is a React component. We check if there’s an authentication token present in local storage, and if it is, then we render our route, the Component , that requires authentication. Otherwise we redirect to a route that doesn’t require authentication.

In this article, we will make a simple Bitbucket app with authenticated React routes. We will let users sign up for an account and set their Bitbucket username and password for their account. Once the user is logged in and set their Bitbucket credentials, they can view their repositories and the commits for each repository.

Bitbucket is a great repository hosting service that lets you host Git repositories for free. You can upgrade to their paid plans to get more features for a low price. It also has a comprehensive API for automation and getting data.

Developers have made Node.js clients for Bitbucket’s API. We can easily use it to do what we want like manipulating commits, adding, removing, or editing repositories, tracking issues, manipulating build pipelines and a lot more.

The Bitbucket.js package is one of the easiest packages to use for writing Node.js Bitbucket apps. All we have to do is log in with the Bitbucket instance with our Bitbucket username and password and call the built in functions listed at https://bitbucketjs.netlify.com/#api-_ to do almost anything we want with the Bitbucket packages.

Our app will consist of an back end and a front end. The back end handles the user authentication and communicates with Bitbucket via the Bitbucket API. The front end has the sign up form, log in form, a settings form for setting password and Bitbucket username and password.

To start building the app, we create a project folder with the backend folder inside. We then go into the backend folder and run the Express Generator by running npx express-generator . Next we install some packages ourselves. We need Babel to use import , BCrypt for hashing passwords, Bitbucket.js for using the Bitbucket API. Crypto-JS for encrypting and decrypting our Bitbucket password, Dotenv for storing hash and encryption secrets, Sequelize for ORM, JSON Web Token packages for authentication, CORS for cross domain communication and SQLite for database.

Run npm i @babel/cli @babel/core @babel/node @babel/preset-env bcrypt bitbucket cors crypto-js dotenv jsonwebtoken sequelize sqlite3 to install the packages.

In the script section of package.json , put:

"start": "nodemon --exec npm run babel-node --  ./bin/www",
"babel-node": "babel-node"

to start our app with Babel Node instead of the regular Node.js runtime so that we get the latest JavaScript features.

Then we create a file called .babelrc in the backend folder and add:

{
    "presets": [
        "@babel/preset-env"
    ]
}

to enable the latest features.

Then we have to add Sequelize code by running npx sequelize-cli init . After that we should get a config.json file in the config folder.

In config.json , we put:

{
  "development": {
    "dialect": "sqlite",
    "storage": "development.db"
  },
  "test": {
    "dialect": "sqlite",
    "storage": "test.db"
  },
  "production": {
    "dialect": "sqlite",
    "storage": "production.db"
  }
}

To use SQLite as our database.

Next we add a middleware for verifying the authentication token, add a middllewares folder in the backend folder, and in there add authCheck.js . In the file, add:

const jwt = require("jsonwebtoken");
const secret = process.env.JWT_SECRET;

export const authCheck = (req, res, next) => {
  if (req.headers.authorization) {
    const token = req.headers.authorization;
    jwt.verify(token, secret, (err, decoded) => {
      if (err) {
        res.send(401);
      } else {
        next();
      }
    });
  } else {
    res.send(401);
  }
};

We return 401 response if the token is invalid.

Next we create some migrations and models. Run:

npx sequelize-cli model:create --name User --attributes username:string,password:string,bitBucketUsername:string,bitBucketPassword:string

to create the model. Note that the attributes option has no spaces.

Then we add unique constraint to the username column of the Users table. To do this, run:

npx sequelize-cli migration:create addUniqueConstraintToUser

Then in newly created migration file, add:

"use strict";

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.addConstraint("Users", ["username"], {
      type: "unique",
      name: "usernameUnique"
    });
  },

down: (queryInterface, Sequelize) => {
    return queryInterface.removeConstraint("Users", "usernameUnique");
  }
};

Run npx sequelize-cli db:migrate to run all the migrations.

Next we create the routes. Create a file called bitbucket.js and add:

var express = require("express");
const models = require("../models");
const CryptoJS = require("crypto-js");
const Bitbucket = require("bitbucket");
const jwt = require("jsonwebtoken");
import { authCheck } from "../middlewares/authCheck";

const bitbucket = new Bitbucket();
var router = express.Router();

router.post("/setBitbucketCredentials", authCheck, async (req, res, next) => {
  const { bitBucketUsername, bitBucketPassword } = req.body;
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const cipherText = CryptoJS.AES.encrypt(
    bitBucketPassword,
    process.env.CRYPTO_SECRET
  );
  await models.User.update(
    {
      bitBucketUsername,
      bitBucketPassword: cipherText.toString()
    },
    {
      where: { id }
    }
  );
  res.json({});
});

router.get("/repos/:page", authCheck, async (req, res, next) => {
  const page = req.params.page || 1;
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const users = await models.User.findAll({ where: { id } });
  const user = users[0];
  const bytes = CryptoJS.AES.decrypt(
    user.bitBucketPassword.toString(),
    process.env.CRYPTO_SECRET
  );
  const password = bytes.toString(CryptoJS.enc.Utf8);
  bitbucket.authenticate({
    type: "basic",
    username: user.bitBucketUsername,
    password
  });
  let { data } = await bitbucket.repositories.list({
    username: user.bitBucketUsername,
    page,
    sort: "-updated_on"
  });
  res.json(data);
});

router.get("/commits/:repoName", authCheck, async (req, res, next) => {
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const users = await models.User.findAll({ where: { id } });
  const user = users[0];
  const repoName = req.params.repoName;
  const bytes = CryptoJS.AES.decrypt(
    user.bitBucketPassword.toString(),
    process.env.CRYPTO_SECRET
  );
  const password = bytes.toString(CryptoJS.enc.Utf8);
  bitbucket.authenticate({
    type: "basic",
    username: user.bitBucketUsername,
    password
  });
  let { data } = await bitbucket.commits.list({
    username: user.bitBucketUsername,
    repo_slug: repoName,
    sort: "-date"
  });
  res.json(data);
});

module.exports = router;

In each route, we get the user from the token, since we will add the user ID into the token, and from there we get the Bitbucket username and password, which we use to log into the Bitbucket API. Note that we have to decrypt the password since we encrypted it before saving it to the database.

We set the Bitbucket credentials in the setBitbucketCredentials route. We encrypt the password before saving to keep it secure.

Then in the repos route, we get the repos of the user and sort by reversed update_on order since we specified -updated_on in the sort parameter. The commits are listed in reverse date order since we specified -date in the sort parameter.

Next we add the user.js in the routes folder and add:

const express = require("express");
const models = require("../models");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
import { authCheck } from "../middlewares/authCheck";
const router = express.Router();

router.post("/signup", async (req, res, next) => {
  const { username, password } = req.body;
  const hashedPassword = await bcrypt.hash(password, 10);
  const user = await models.User.create({ username, password: hashedPassword });
  res.json(user);
});

router.post("/login", async (req, res, next) => {
  const { username, password } = req.body;
  const users = await models.User.findAll({ where: { username } });
  const user = users[0];
  await bcrypt.compare(password, user.password);
  const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
  res.json({ token });
});

router.post("/changePassword", authCheck, async (req, res, next) => {
  const { password } = req.body;
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const hashedPassword = await bcrypt.hash(password, 10);
  await models.User.update({ password: hashedPassword }, { where: { id } });
  res.json({});
});

router.get("/currentUser", authCheck, async (req, res, next) => {
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const users = await models.User.findAll({ where: { id } });
  const { username, bitBucketUsername } = users[0];
  res.json({ username, bitBucketUsername });
});

module.exports = router;

We have routes for sign up, log in, and change password. We hash the password before saving when we sign up or changing password.

The currentUser route will be used for a settings form in the front end.

In app.js we replace the existing code with:

require("dotenv").config();
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
const cors = require("cors");

var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
var bitbucketRouter = require("./routes/bitbucket");

var app = express();

// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");

app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(cors());

app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/bitbucket", bitbucketRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};

// render the error page
  res.status(err.status || 500);
  res.render("error");
});

module.exports = app;

to add the CORS add-on to enable cross domain communication, and add the users and bitbucket routes in our app by adding:

app.use("/users", usersRouter);
app.use("/bitbucket", bitbucketRouter);

This finishes the back end portion of the app. Now we can build the front end. We will build it with React, so we start by running npx create-react-app frontend from the project’s root folder.

Next we have to install some packages. We will install Bootstrap for styling, React Router for routing, Formik and Yup for form value handling and form validation respectively and Axios for making HTTP requests.

To do this run npm i axios bootstrap formik react-bootstrap react-router-dom yup to install all the packages.

Next we modify the App.js folder by replacing the existing code with the following:

import React from "react";
import HomePage from "./HomePage";
import "./App.css";
import ReposPage from "./ReposPage";
import CommitsPage from "./CommitsPage";
import SettingsPage from "./SettingsPage";
import { createBrowserHistory as createHistory } from "history";
import { Router, Route } from "react-router-dom";
import SignUpPage from "./SignUpPage";
import RequireAuth from "./RequireAuth";
const history = createHistory();

function App() {
  return (
    <div className="App">
      <Router history={history}>
        <Route path="/" exact component={HomePage} />
        <Route path="/signup" exact component={SignUpPage} />
        <Route
          path="/settings"
          component={props => (
            <RequireAuth {...props} Component={SettingsPage} />
          )}
        />
        <Route
          path="/repos"
          exact
          component={props => <RequireAuth {...props} Component={ReposPage} />}
        />
        <Route
          path="/commits/:repoName"
          exact
          component={props => (
            <RequireAuth {...props} Component={CommitsPage} />
          )}
        />
      </Router>
    </div>
  );
}

export default App;

We add the routes for the pages in our app here. RequiredAuth is a higher order component which takes a component that requires authentication to access and return redirect if not authorized or the page if the user is authorized. We pass in the components that requires authentication in this route, like ReposPage , SettingsPage and CommitPage .

In App.css , we replace the existing code with:

.page {
    padding: 20px;
}

to add some padding.

Next we add CommitsPage.js in the src folder and add:

import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import { commits } from "./requests";
import Card from "react-bootstrap/Card";
import LoggedInTopBar from "./LoggedInTopBar";
const moment = require("moment");

function CommitsPage({ match: { params } }) {
  const [initialized, setInitialized] = useState(false);
  const [repoCommits, setRepoCommits] = useState([]);

const getCommits = async page => {
    const repoName = params.repoName;
    const response = await commits(repoName, page);
    setRepoCommits(response.data.values);
  };

useEffect(() => {
    if (!initialized) {
      getCommits(1);
      setInitialized(true);
    }
  });

return (
    <>
      <LoggedInTopBar />
      <div className="page">
        <h1 className="text-center">Commits</h1>
        {repoCommits.map(rc => {
          return (
            <Card style={{ width: "90vw", margin: "0 auto" }}>
              <Card.Body>
                <Card.Title>{rc.message}</Card.Title>
                <p>Message: {rc.author.raw}</p>
                <p>
                  Date:{" "}
                  {moment(rc.date).format("dddd, MMMM Do YYYY, h:mm:ss a")}
                </p>
                <p>Hash: {rc.hash}</p>
              </Card.Body>
            </Card>
          );
        })}
      </div>
    </>
  );
}

export default withRouter(CommitsPage);

We get the commits given the repository name in the URL parameter and display them in a list of Cards provided by React Bootstrap.

Next we create the Home Page, which lets us sign in. Create a HomePage.js file in the src folder and add:

import React, { useState, useEffect } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { Link } from "react-router-dom";
import * as yup from "yup";
import { logIn } from "./requests";
import { Redirect } from "react-router-dom";
import Navbar from "react-bootstrap/Navbar";

const schema = yup.object({
  username: yup.string().required("Username is required"),
  password: yup.string().required("Password is required")
});

function HomePage() {
  const [redirect, setRedirect] = useState(false);

const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    try {
      const response = await logIn(evt);
      localStorage.setItem("token", response.data.token);
      setRedirect(true);
    } catch (ex) {
      alert("Invalid username or password");
    }
  };

if (redirect) {
    return <Redirect to="/repos" />;
  }

return (
    <>
      <Navbar bg="primary" expand="lg" variant="dark">
        <Navbar.Brand href="#home">Bitbucket App</Navbar.Brand>
      </Navbar>
      <div className="page">
        <h1 className="text-center">Log In</h1>
        <Formik validationSchema={schema} onSubmit={handleSubmit}>
          {({
            handleSubmit,
            handleChange,
            handleBlur,
            values,
            touched,
            isInvalid,
            errors
          }) => (
            <Form noValidate onSubmit={handleSubmit}>
              <Form.Row>
                <Form.Group as={Col} md="12" controlId="username">
                  <Form.Label>Username</Form.Label>
                  <Form.Control
                    type="text"
                    name="username"
                    placeholder="Username"
                    value={values.username || ""}
                    onChange={handleChange}
                    isInvalid={touched.username && errors.username}
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.username}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="password">
                  <Form.Label>Password</Form.Label>
                  <Form.Control
                    type="password"
                    name="password"
                    placeholder="Password"
                    value={values.password || ""}
                    onChange={handleChange}
                    isInvalid={touched.password && errors.password}
                  />

<Form.Control.Feedback type="invalid">
                    {errors.password}
                  </Form.Control.Feedback>
                </Form.Group>
              </Form.Row>
              <Button type="submit" style={{ marginRight: "10px" }}>
                Log In
              </Button>
              <Link
                className="btn btn-primary"
                to="/signup"
                style={{ marginRight: "10px", color: "white" }}
              >
                Sign Up
              </Link>
            </Form>
          )}
        </Formik>
      </div>
    </>
  );
}

export default HomePage;

We use the Formik component provided by Formik to get automatic form value handling. It will pipe the values directly to the parameter of the onSubmit handler. In the handleSubmit function, we run the schema.validate function to check for validity of the form values according to the schema object generated from the Yup library and if that’s successful, then we call login from the requests.js file which we will create.

Next we create the top bar. Create LoggedInTopBar.js in the src folder and add:

import React, { useState } from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter, Redirect } from "react-router-dom";

function LoggedInTopBar({ location }) {
  const [redirect, setRedirect] = useState(false);
  const { pathname } = location;

  const isLoggedIn = () => !!localStorage.getItem("token");

  if (redirect) {
    return <Redirect to="/" />;
  }

  return (
    <div>
      {isLoggedIn() ? (
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Bitbucket App</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/settings" active={pathname == "/settings"}>
                Settings
              </Nav.Link>
              <Nav.Link href="/repos" active={pathname == "/repos"}>
                Repos
              </Nav.Link>
              <Nav.Link>
                <span
                  onClick={() => {
                    localStorage.clear();
                    setRedirect(true);
                  }}
                >
                  Log Out
                </span>
              </Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Navbar>
      ) : null}
    </div>
  );
}

export default withRouter(LoggedInTopBar);

This contains the React Bootstrap Navbar to show a top bar with a link to the home page and the name of the app. We only display it with the token present in local storage. We check the pathname to highlight the right links by setting the active prop.

Next create ReposPage.js in the src folder to display the list of repositories the user owns. We add:

import React, { useState, useEffect } from "react";
import { repos } from "./requests";
import Card from "react-bootstrap/Card";
import { Link } from "react-router-dom";
import Pagination from "react-bootstrap/Pagination";
import LoggedInTopBar from "./LoggedInTopBar";

function ReposPage() {
  const [initialized, setInitialized] = useState(false);
  const [repositories, setRepositories] = useState([]);
  const [page, setPage] = useState(1);
  const [totalPages, setTotalPages] = useState(1);

  const getRepos = async page => {
    const response = await repos(page);
    setRepositories(response.data.values);
    setTotalPages(Math.ceil(response.data.size / response.data.pagelen));
  };

  useEffect(() => {
    if (!initialized) {
      getRepos(1);
      setInitialized(true);
    }
  });

  return (
    <div>
      <LoggedInTopBar />
      <h1 className="text-center">Your Repositories</h1>
      {repositories.map((r, i) => {
        return (
          <Card style={{ width: "90vw", margin: "0 auto" }} key={i}>
            <Card.Body>
              <Card.Title>{r.slug}</Card.Title>
              <Link className="btn btn-primary" to={`/commits/${r.slug}`}>
                Go
              </Link>
            </Card.Body>
          </Card>
        );
      })}
      <br />
      <Pagination style={{ width: "90vw", margin: "0 auto" }}>
        <Pagination.First onClick={() => getRepos(1)} />
        <Pagination.Prev
          onClick={() => {
            let p = page - 1;
            getRepos(p);
            setPage(p);
          }}
        />
        <Pagination.Next
          onClick={() => {
            let p = page + 1;
            getRepos(p);
            setPage(p);
          }}
        />
        <Pagination.Last onClick={() => getRepos(totalPages)} />
      </Pagination>
      <br />
    </div>
  );
}

export default ReposPage;

We get the list repositories and we add pagination to get more repositories.

In requests.js , we add:

const axios = require("axios");
const APIURL = "http://localhost:3000";

axios.interceptors.request.use(
  config => {
    config.headers.authorization = localStorage.getItem("token");
    return config;
  },
  error => Promise.reject(error)
);

axios.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    if (error.response.status == 401) {
      localStorage.clear();
    }
    return error;
  }
);

export const signUp = data => axios.post(`${APIURL}/users/signup`, data);

export const logIn = data => axios.post(`${APIURL}/users/login`, data);

export const changePassword = data =>
  axios.post(`${APIURL}/users/changePassword`, data);

export const currentUser = () => axios.get(`${APIURL}/users/currentUser`);

export const setBitbucketCredentials = data =>
  axios.post(`${APIURL}/bitbucket/setBitbucketCredentials`, data);

export const repos = page =>
  axios.get(`${APIURL}/bitbucket/repos/${page || 1}`);

export const commits = (repoName) =>
  axios.get(`${APIURL}/bitbucket/commits/${repoName}`);

This file has all the HTTP requests that we make for sign up, log in, set credentials, get repositories and commits, etc.

And for handling responses, if we get 401 responses then we clear local storage so we won’t be using an invalid token. The code is below:

axios.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    if (error.response.status == 401) {
      localStorage.clear();
    }
    return error;
  }
);

We attach the token to the request headers with:

axios.interceptors.request.use(
  config => {
    config.headers.authorization = localStorage.getItem("token");
    return config;
  },
  error => Promise.reject(error)
);

so we don’t have to set it for each authenticated requests.

Next create a file called RequiredAuth.js in the src folder and add:

import React from "react";
import { Redirect } from "react-router-dom";

function RequireAuth({ Component }) {
  if (!localStorage.getItem("token")) {
    return <Redirect to="/" />;
  }
  return <Component />;
}

export default RequireAuth;

We check for presence of the token in the authenticated routes and render the Component prop passed in if the token is present.

Then we create a file called SettingsPage.js in the src folder and add:

import React, { useState, useEffect } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import {
  currentUser,
  setBitbucketCredentials,
  changePassword
} from "./requests";
import LoggedInTopBar from "./LoggedInTopBar";

const userFormSchema = yup.object({
  username: yup.string().required("Username is required"),
  password: yup.string().required("Password is required")
});

const bitBucketFormSchema = yup.object({
  bitBucketUsername: yup.string().required("Username is required"),
  bitBucketPassword: yup.string().required("Password is required")
});

function SettingsPage() {
  const [initialized, setInitialized] = useState(false);
  const [user, setUser] = useState({});
  const [bitbucketUser, setBitbucketUser] = useState({});

  const handleUserSubmit = async evt => {
    const isValid = await userFormSchema.validate(evt);
    if (!isValid) {
      return;
    }
    try {
      await changePassword(evt);
      alert("Password changed");
    } catch (error) {
      alert("Password change failed");
    }
  };

  const handleBitbucketSubmit = async evt => {
    const isValid = await bitBucketFormSchema.validate(evt);
    if (!isValid) {
      return;
    }
    try {
      await setBitbucketCredentials(evt);
      alert("Bitbucket credentials changed");
    } catch (error) {
      alert("Bitbucket credentials change failed");
    }
  };

  const getCurrentUser = async () => {
    const response = await currentUser();
    const { username, bitBucketUsername } = response.data;
    setUser({ username });
    setBitbucketUser({ bitBucketUsername });
  };

  useEffect(() => {
    if (!initialized) {
      getCurrentUser();
      setInitialized(true);
    }
  });

  return (
    <>
      <LoggedInTopBar />
      <div className="page">
        <h1 className="text-center">Settings</h1>
        <h2>User Settings</h2>
        <Formik
          validationSchema={userFormSchema}
          onSubmit={handleUserSubmit}
          initialValues={user}
          enableReinitialize={true}
        >
          {({
            handleSubmit,
            handleChange,
            handleBlur,
            values,
            touched,
            isInvalid,
            errors
          }) => (
            <Form noValidate onSubmit={handleSubmit}>
              <Form.Row>
                <Form.Group as={Col} md="12" controlId="username">
                  <Form.Label>Username</Form.Label>
                  <Form.Control
                    type="text"
                    name="username"
                    placeholder="Username"
                    value={values.username || ""}
                    onChange={handleChange}
                    isInvalid={touched.username && errors.username}
                    disabled
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.username}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="password">
                  <Form.Label>Password</Form.Label>
                  <Form.Control
                    type="password"
                    name="password"
                    placeholder="Password"
                    value={values.password || ""}
                    onChange={handleChange}
                    isInvalid={touched.password && errors.password}
                  />

                  <Form.Control.Feedback type="invalid">
                    {errors.password}
                  </Form.Control.Feedback>
                </Form.Group>
              </Form.Row>
              <Button type="submit" style={{ marginRight: "10px" }}>
                Save
              </Button>
            </Form>
          )}
        </Formik>

        <br />
        <h2>BitBucket Settings</h2>
        <Formik
          validationSchema={bitBucketFormSchema}
          onSubmit={handleBitbucketSubmit}
          initialValues={bitbucketUser}
          enableReinitialize={true}
        >
          {({
            handleSubmit,
            handleChange,
            handleBlur,
            values,
            touched,
            isInvalid,
            errors
          }) => (
            <Form noValidate onSubmit={handleSubmit}>
              <Form.Row>
                <Form.Group as={Col} md="12" controlId="bitBucketUsername">
                  <Form.Label>BitBucket Username</Form.Label>
                  <Form.Control
                    type="text"
                    name="bitBucketUsername"
                    placeholder="BitBucket Username"
                    value={values.bitBucketUsername || ""}
                    onChange={handleChange}
                    isInvalid={
                      touched.bitBucketUsername && errors.bitBucketUsername
                    }
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.bitBucketUsername}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="bitBucketPassword">
                  <Form.Label>Password</Form.Label>
                  <Form.Control
                    type="password"
                    name="bitBucketPassword"
                    placeholder="Bitbucket Password"
                    value={values.bitBucketPassword || ""}
                    onChange={handleChange}
                    isInvalid={
                      touched.bitBucketPassword && errors.bitBucketPassword
                    }
                  />

<Form.Control.Feedback type="invalid">
                    {errors.bitbucketPassword}
                  </Form.Control.Feedback>
                </Form.Group>
              </Form.Row>
              <Button type="submit" style={{ marginRight: "10px" }}>
                Save
              </Button>
            </Form>
          )}
        </Formik>
      </div>
    </>
  );
}

export default SettingsPage;

We have 2 forms. One for setting the password of the user and another for saving the Bitbucket credentials. We valid both in separate Yup schemas and make the request to back end if the data entered is valid.

Next we create the sign up page by creating SignUpPage.js in the src folder. We add:

import React, { useState, useEffect } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { Redirect } from "react-router-dom";
import * as yup from "yup";
import { signUp } from "./requests";
import Navbar from "react-bootstrap/Navbar";

const schema = yup.object({
  username: yup.string().required("Username is required"),
  password: yup.string().required("Password is required")
});

function SignUpPage() {
  const [redirect, setRedirect] = useState(false);

  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    try {
      await signUp(evt);
      setRedirect(true);
    } catch (ex) {
      alert("Username already taken");
    }
  };

  if (redirect) {
    return <Redirect to="/" />;
  }

  return (
    <>
      <Navbar bg="primary" expand="lg" variant="dark">
        <Navbar.Brand href="#home">Bitbucket App</Navbar.Brand>
      </Navbar>
      <div className="page">
        <h1 className="text-center">Sign Up</h1>
        <Formik validationSchema={schema} onSubmit={handleSubmit}>
          {({
            handleSubmit,
            handleChange,
            handleBlur,
            values,
            touched,
            isInvalid,
            errors
          }) => (
            <Form noValidate onSubmit={handleSubmit}>
              <Form.Row>
                <Form.Group as={Col} md="12" controlId="username">
                  <Form.Label>Username</Form.Label>
                  <Form.Control
                    type="text"
                    name="username"
                    placeholder="Username"
                    value={values.username || ""}
                    onChange={handleChange}
                    isInvalid={touched.username && errors.username}
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.username}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="password">
                  <Form.Label>Password</Form.Label>
                  <Form.Control
                    type="password"
                    name="password"
                    placeholder="Password"
                    value={values.password || ""}
                    onChange={handleChange}
                    isInvalid={touched.password && errors.password}
                  />

                  <Form.Control.Feedback type="invalid">
                    {errors.password}
                  </Form.Control.Feedback>
                </Form.Group>
              </Form.Row>
              <Button type="submit" style={{ marginRight: "10px" }}>
                Sign Up
              </Button>
            </Form>
          )}
        </Formik>
      </div>
    </>
  );
}

export default SignUpPage;

It’s similar to the other log in request, except we call the sign up request instead of log in.

Finally, in index.html , we replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Bitbucket App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

to add the Bootstrap CSS and change the title.

After writing all that code, we can run our app. Before running anything, install nodemon by running npm i -g nodemon so that we don’t have to restart back end ourselves when files change.

Then run back end by running npm start in the backend folder and npm start in the frontend folder, then choose ‘yes’ if you’re asked to run it from a different port.

Then you get:

Categories
JavaScript Answers

Why Do We Need Strict Mode in JavaScript?

Strict mode is an important part of modern JavaScript. It is a mode that lets us opt-in to a more restricted syntax of JavaScript.

The semantics of strict mode is different from the old “sloppy mode” of JavaScript that has a looser syntax and makes errors in code that are silenced. This means that errors are ignored and the code might run with unexpected results.

Strict mode makes several changes to JavaScript semantics. It eliminates silent errors and instead throws them so that the code won’t run with errors in the code.

It will also point out mistakes that prevent JavaScript engines from doing optimizations. Also, it prohibits features that are likely to be defined in future versions of JavaScript.

Strict mode can be applied to individual functions or the whole script. It can’t be applied only to statements or other blocks enclosed in curly braces. To make a script use strict mode, we add the statement "use strict" or 'use strict' at the top of the script before any other statements.

If we have some scripts that use strict mode and others that don’t, then we might have scripts that use strict mode that are concatenated with other scripts that don’t use strict mode.

This means that code that doesn’t use strict mode might have been made to use strict mode when it’s concatenated together. The reverse is also true. So, it’s best not to mix them together.

We can also apply this to functions. To do this, we add the statement "use strict" or 'use strict' inside the functions on the top of the body, before any other statements. It’s applied to everything inside, including functions that are nested in the function that uses strict mode.

For example:

const strictFunction = ()=>{
  'use strict';
  const nestedFunction = ()=>{
    // this function also uses strict mode
  }
}

JavaScript modules, introduced in ES2015, have strict mode automatically enabled, so no statement is needed to enable it.

Changes in Strict Mode

Strict mode changes both the syntax and run time behavior of code that uses it. Mistakes in code are converted to errors as syntax errors that are thrown in run time, simplify how particular variables are computed, changes that simplify the eval function and the arguments object, and changes that might be implemented in future ES specification.

Converting Mistakes to Errors

Mistakes are converted to errors. They were previously accepted in non-strict mode. Strict mode restricts the use of error syntax and will not let the code run with errors in place.

It makes creating global variables hard by not letting us declare variables with var, let, or const, so creating variables without declaring them with those keywords won’t work. For example, the following code will throw a ReferenceError:

'use strict';
badVariable = 1;

We can’t run the code above in strict mode because this code will create a global variable badVariable if strict mode is off. Strict mode prevents this to prevent the creation of global variables accidentally.

Any code that silently fails will now throw an exception. This includes any invalid syntax that was ignored silently before.

For example, we can’t not assign to read-only variables like arguments, NaN, or eval with strict mode on.

Any assignment to read-only properties like non-writable global properties, assignment of getter-only properties, and assigning things to properties on non-extendable objects will throw an exception in strict mode.

Below are some examples of syntax that will fail with strict mode on:

'use strict';

let undefined = 5;
let Infinity = 5;

let obj = {};
Object.defineProperty(obj, 'foo', { value: 1, writable: false });
obj.foo = 1

let obj2 = { get foo() { return 17; } };
obj2.foo = 2

let fixedObj = {};
Object.preventExtensions(fixedObj);
fixed.bar= 1;

All of the examples above will throw a TypeError. undefined and Infinity are non-writable global objects. obj is a non-writable property.

obj2’s foo property is a getter only property and so cannot be set. fixedObj was prevented from adding more properties to it with the Object.preventExtensions method.

Also, deleting undeletable properties will throw a TypeError when there is code attempting to do so. For example:

'use strict';
delete `Array`.prototype

This will throw a TypeError.

Strict mode also disallows duplicate property names in an object before ES6 is introduced, so the following example will throw a syntax error:

'use strict';
var o = { a: 1, a: 2 };

Strict mode requires that function parameter names are unique. Without strict mode, if two parameters have the name one, then the one defined later will be accepted as the parameter’s value when arguments are passed in.

With strict mode, having multiple function parameters with the same name is no longer allowed, so the following example will fail to run with a syntax error:

const multiply = (x, x, y) => x*x*y;

Octal syntax is also not allowed in strict mode. It isn’t part of the specification, but it’s supported in browsers by prefixing octal numbers with a 0.

This confuses developers as some may think that the 0 preceding the number is meaningless. Therefore, strict mode disallows this syntax and will throw a syntax error.

Strict mode also prevents the use of syntax that makes optimizations difficult. It needs to know that a variable is actually stored at the location that it thinks it’s stored at before doing optimization, so we have to prevent the kind of syntax that stops optimizations from happening.

One example of this is the with statement. If we use it, it prevents the JavaScript interpreter from knowing which variable or property you’re referring to, as it’s possible to have a variable with the same name inside or outside the with statement.

If we have something like the following code:

let x = 1;
with (obj) {
  x;
}

Then, JavaScript wouldn’t know if the x inside the with statement is referring to the x variable or the property of obj, obj.x.

Therefore, the memory location of x is ambiguous. So, strict mode prevents the with statement from being used. If we have strict mode like the following:

'use strict';
`let x = 1;
with (obj) {
  x;
}`

Then the code above will have a syntax error.

Another thing that strict mode prevents is the declaration of variables inside an eval statement.

For example, without strict mode, eval('let x') would declare the variable x into the code. This lets people hide variable declarations in strings that might override the same variable declaration that’s outside the eval statement.

To prevent this, strict mode disallows variable declarations in the string argument that we pass into an eval statement.

Strict mode also prohibits deletion of plain variable names, so the following will throw a syntax error:

'use strict';

let x;
delete x;

Prohibiting Invalid Syntax

Invalid syntax of eval and argument isn’t allowed in strict mode.

This means that any manipulation done to them that isn’t allowed, like assigning new values to them or using them as names of variables, functions, or parameters in functions.

The following are examples of invalid uses of eval and the argument object that isn’t allowed:

'use strict';
eval = 1;
arguments++;
arguments--;
++eval;
eval--;
let obj = { set p(arguments) { } };
let eval;
try { } catch (arguments) { }
try { } catch (eval) { }
function x(eval) { }
function arguments() { }
let y = function eval() { };
let eval = ()=>{ };
let f = new Function('arguments', "'use strict'; return 1;");

Strict mode doesn’t allow an alias for the arguments object to be created and set with new values via the alias.

Without strict mode, if the first parameter of a function is a, then setting a also sets arguments[0]. With strict mode, the arguments object will always have the list of arguments that the function is called with.

For example, if we have:

const fn = function(a) {
  'use strict';
  a = 2;
  return [a, arguments[0]];
}

console.log(fn(1))

Then we should see [2,1] logged. This is because setting a to 2 doesn’t also set arguments[0] to 2.

Performance Optimizations

Also, there’s no more support for arguments.callee. Without strict mode, all it does is return the name of the function that’s being called that the arguments.callee is in.

It prevents optimizations like inline functions because arguments.callee requires that a reference to the un-inlined function is available if arguments.callee is accessed. Therefore, with strict mode, arguments.callee now throws a TypeError.

With strict mode, this will not be forced into always being an object. If a function’s this is bound, with call, apply, or bind to any non-object type, like primitive types of undefined, null, number, boolean, etc, then they must be forced into being objects.

If the context of this is switched to a non-object type, then the global window object will take its place. This means that the global object is exposed to the function that’s being called with this being bound to non-object types.

For example, if we run the following code:

'use strict';

function fn() {
  return this;
}
console.log(fn() === undefined);
console.log(fn.call(2) === 2);
console.log(fn.apply(null) === null);
console.log(fn.call(undefined) === undefined);
console.log(fn.bind(true)() === true);

All the console logs will be true, as the this inside the function isn’t automatically converted into the window global object when this is changed to something that has a non-object type.

Security Fixes

In strict mode, we also don’t allow the function’s caller and arguments to be exposed to the public since caller may expose the function that calls the function that the function’s caller property accessed.

The arguments have arguments that are passed in when the function is called. For example, if we have a function called fn, then through fn.caller, we can see the function that called fn, and through fn.arguments, we can see the arguments that were passed into fn when the call to fn is made.

This is a potential security hole that is eliminated by prohibiting access to these two properties of the function.

function secretFunction() {
  'use strict';
  secretFunction.caller;
  secretFunction.arguments;
}
function restrictedRunner() {
  return secretFunction();
}
restrictedRunner();

In the example above, we can’t access secretFunction.caller and secretFunction.arguments with strict mode as people may use it to get the call stack of functions. A TypeError will be thrown if we run that code.

Identifiers that will become restricted keywords in future versions of JavaScript will not be allowed to be used for identifiers like variable or property names.

The following keywords won’t be allowed to be used for defining our identifiers in code. implements, interface, let, package, private, protected, public, static, and yield.

With ES2015 or later, these have become reserved words, so they definitely cannot be used for naming variables and object properties without strict mode.

Strict mode has been a standard for a few years. Browser support for it is common, and only old browsers like Internet Explorer may have problems with it.

Other browsers shouldn’t have problems with working with strict mode. Therefore, it should be used to prevent mistakes and avoiding security hazards like exposing call stacks or declaring new variables in eval.

Also, it eliminates silent errors and instead throws them so that the code won’t run with errors in the code. It will also point out mistakes that prevent JavaScript engines to do optimizations.

Also, it prohibits features that are likely to be defined in future versions of JavaScript.

Categories
JavaScript Answers

Using Promises for Async Programming in JavaScript

Because JavaScript is a single-threaded language, synchronous code can only be executed one line at a time. This means that if there’s synchronous code that runs longer than instantaneously, then it will stop the rest of the code from running until whatever is being run is finished. To prevent code that runs for an indeterminate amount of time from holding up other code from running, we need to have asynchronous code.


Promises

To do this in JavaScript, we can use promises in our code. Promises are objects that represent a process that runs for an indeterminate amount of time and may result in success or failure. To create promises in JavaScript, we use a Promise object that’s a constructor for creating promises. The Promise constructor takes a function called the executor function that has the resolve and reject parameters. Both parameters are functions that we call to either fulfill the promise (which means end it successfully with some value) or to reject the promise (which means returning some error value and marking the promise as failed). The function’s return value is ignored. Therefore, promises can’t return anything other than promises.

For example, we can define a promise in JavaScript like in the following code:

const promise = new Promise((resolve, reject) => {  
  setTimeout(() => resolve('abc'), 1000);  
});

The code above creates a promise that fulfills the promise with the value abc after one second. Because we’re running setTimeout inside the executor function to resolve the promise with the value abc in one second, it’s asynchronous code. We can’t return the value abc within the callback function of setTimeout, so we have to call resolve('abc') to get the resolved value back. We can access the resolved value of the fulfilled promise by using the then function. The then function takes a callback function that takes the resolved value of the fulfilled promise as a parameter. You can get the value there and do what you want with it. For example, we can do something like:

const promise = new Promise((resolve, reject) => {  
  setTimeout(() => resolve('abc'), 1000);  
});

promise.then((val) => {  
  console.log(val);  
})

When we run the code above, we should see abc logged. As we can see, the promise will supply the value when the promise is fulfilled with a call to the resolve function.

A promise has three states. It can be pending, which means that the promise has been fulfilled or rejected. It can be fulfilled, which means that the operation completed successfully. Or it can be rejected, which means that the promise operation failed.

A pending promise can either be fulfilled with a value or be rejected with some error. When a promise is fulfilled, then the corresponding resolve value is picked up by the then function and the callback function passed into the then function is called. If a promise is rejected, then we can optionally catch the error with the catch function, which also takes a callback function with the error. Both the then and catch functions return promises, so they can be chained together.

For example, we can write something like:

const promise = (num) => {  
  return new Promise((resolve, reject) => {  
    setTimeout(() => {  
      if (num === 1) {  
        resolve('resolved')  
      } else {  
        reject('rejected')  
      }  
    }, 1000);  
  });  
}

promise(1)  
  .then((val) => {  
    console.log(val);  
  })  
  .catch((error) => {  
    console.log(error);  
  })promise(2)  
  .then((val) => {  
    console.log(val);  
  })  
  .catch((error) => {  
    console.log(error);  
  })

In the code above, we have a function promise that returns a JavaScript promise that fulfills the promise with the value resolved when num is 1, and reject the promise with error rejected otherwise. So we run:

promise(1)  
  .then((val) => {  
    console.log(val);  
  })  
  .catch((error) => {  
    console.log(error);  
  })

Then the then function runs, the promise returned by the promise(1) function call is fulfilled since num is 1, and the resolved value is available set in val. So when we run console.log(val) we get resolved logged. When we run the code below:

promise(2)  
  .then((val) => {  
    console.log(val);  
  })  
  .catch((error) => {  
    console.log(error);  
  })

Then the catch function runs because the promise returned by the promise(2) function call is rejected, and the rejected error value is available set in error. So when we run console.log(error) we get rejected logged.

A JavaScript promise object has the following properties: length and prototype. The length always has its value set to 1 for the number of constructor arguments, which is always one. The prototype property represents the prototype of the promise object.

A promise also has the finally method to run code that’s run no matter whether the promise is fulfilled or rejected. The finally method takes a callback function as an argument, where any code that you want to run, regardless of the outcome of the promise, is run. For example, if we run:

Promise.reject('error')  
  .then((value) => {  
    console.log(value);  
  })  
  .catch((error) => {  
    console.log(error);  
  })  
  .finally(() => {  
    console.log('finally runs');  
  })

Then we get error and finally runs logged because the original promise was rejected with the reason error. Then whatever code in the callback of the finally method runs.

The main benefit of using promises for writing asynchronous code is that we can use them to run asynchronous code sequentially. To do this, we can chain promises with the then function. The then function takes a callback function that runs when the promise is fulfilled. It also takes a second argument when the promise is rejected. To chain promises, we have to return another promise as the return value of the first callback function of the then function. It can return other values, like nothing, if we don’t want to chain another promise to the existing promise. We can return a value, which will be resolved and the value retrievable in the next then function. It can also throw an error. Then the promise returned by then gets rejected with the thrown error as the value. It can also return an already fulfilled or rejected promise, which will get the fulfilled value when a then function is chained after it or get an error reason in the callback of the catch function.

For example, we can write the following:

Promise.resolve(1)  
  .then(val => {  
    console.log(val);  
    return Promise.resolve(2)  
  })  
  .then(val => {  
    console.log(val);  
  })Promise.resolve(1)  
  .then(val => {  
    console.log(val);  
    return Promise.reject('error')  
  })  
  .then(val => {  
    console.log(val);  
  })  
  .catch(error => console.log(error));Promise.resolve(1)  
  .then(val => {  
    console.log(val);  
    throw new Error('error');  
  })  
  .then(val => {  
    console.log(val);  
  })  
  .catch(error => console.log(error));

In the first example, we chained the promises and they all resolve to a value. All the promises were already resolved into values. In the second and last examples, we reject the second promise or throw an error. They both do the same thing. The second promise is rejected, and the error reason will be logged in the callback of the catch function. We can also chain pending promises, like in the following code:

const promise1 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(1), 1000);  
});

const promise2 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(2), 1000);  
});

promise1  
  .then(val => {  
    console.log(val);  
    return promise2;  
  })  
  .then(val => {  
    console.log(val);  
  })  
  .catch(error => console.log(error));

The callback of the first then function returned promise2, which is a pending promise.


Methods

JavaScript promises have the following methods.

Promise.all(iterable)

Promise.all takes an iterable object that lets us run multiple promises in parallel in some computers and serially in other computers. This is handy for running multiple promises that don’t depend on each other’s values. It takes in an iterable with a list of promises, usually an array, and then returns a single Promise that’s resolved when the promises that are in the iterable are resolved.

For example, we can write code like the following to run multiple promises with Promise.all:

const promise1 = Promise.resolve(1);  
const promise2 = 2;  
const promise3 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(3), 1000);  
});  
Promise.all([promise1, promise2, promise3])  
  .then((values) => {  
    console.log(values);  
  });

If we run the code above, then the console.log should log [1,2,3]. As we can see, it only returns a resolved value after all the promises are fulfilled. If some of them are rejected, then we won’t get any resolved value. Instead, we will get whatever error value is returned by the rejected promises. It will stop at the first rejected promise and send that value to the callback of the catch function. For example, if we have:

const promise1 = Promise.resolve(1);  
const promise2 = Promise.reject(2);  
const promise3 = new Promise((resolve, reject) => {  
  setTimeout(() => reject(3), 1000);  
});  
Promise.all([promise1, promise2, promise3])  
  .then((values) => {  
    console.log(values);  
  })  
  .catch(error => {  
    console.log(error);  
  });

Then we get 2 logged from the console.log in the callback of the catch function.

Promise.allSettled

Promise.allSettled returns a promise that resolves after all the given promises are resolved or rejected. It takes an iterable object with a collection of promises, for example, an array of promises. The resolved value of the returned promise is an array of the final status of each promise. For example, suppose we have:

const promise1 = Promise.resolve(1);  
const promise2 = Promise.reject(2);  
const promise3 = new Promise((resolve, reject) => {  
  setTimeout(() => reject(3), 1000);  
});  
Promise.allSettled([promise1, promise2, promise3])  
  .then((values) => {  
    console.log(values);  
  })

If we run the code above, then we get an array with three entries, with each entry being an object that has the status and value properties for the fulfilled promises and an object that has the status and reason properties for the rejected promises. For example, the code above will log {status: “fulfilled”, value: 1}, {status: “rejected”, reason: 2}, {status: “rejected”, reason: 3} . The fulfilled status is logged for the successful promises and therejected status for the rejected ones.

Promise.race

The Promise.race method returns a promise that resolves to the resolved value of the promise that’s fulfilled first. It takes an iterable object with a collection of promises, for example, an array of promises. If the iterable passed in is empty, then the returned promise will forever be pending. If the iterable object contains one or more non-promise values or already settled promises, then Promise.race will return to the first of these entries. For example, if we have:

const promise1 = Promise.resolve(1);  
const promise2 = Promise.resolve(2);  
const promise3 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(3), 1000);  
});  
Promise.race([promise1, promise2, promise3])  
  .then((values) => {  
    console.log(values);  
  })

Then we see that 1 is logged. That’s because promise1 is the first one that’s resolved since it’s resolved instantaneously before the next lines are run. Likewise, if we have a non-promise value in the array that we pass in as the argument, like in the following code:

const promise1 = 1;  
const promise2 = Promise.resolve(2);  
const promise3 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(3), 1000);  
});  
Promise.race([promise1, promise2, promise3])  
  .then((values) => {  
    console.log(values);  
  })

Then we get the same thing logged since it’s the non-promise value in the array that we pass into the Promise.race method. Synchronous code always runs before the asynchronous code, so it doesn’t matter where the synchronous code is. If we have:

const promise1 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(1), 2000);  
});

const promise2 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(2), 1000);  
});

const promise3 = 3;  
Promise.race([promise1, promise2, promise3])  
  .then((values) => {  
    console.log(values);  
  })

Then we get 3 logged since setTimeout puts the callback function in the queue to be run later, so it’s going to be run later than the synchronous code.

Finally, if we have:

const promise1 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(1), 2000);  
}); 
 
const promise2 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(2), 1000);  
});

Promise.race([promise1, promise2])  
  .then((values) => {  
    console.log(values);  
  })

Then we get 2 in the console log because a promise that’s resolved in one second is going to be resolved earlier than a promise resolved in two seconds.

Promise.reject

Promise.reject returns a promise that’s rejected with a reason. It’s useful to reject a promise with an object that’s an instance of Error. For example, if we have the following code:

Promise.reject(new Error('rejected'))  
  .then((value) => {  
    console.log(value);  
  })  
  .catch((error) => {  
    console.log(error);  
  })

Then we get rejected logged.

Promise.resolve

Promise.resolve returns a promise that’s resolved to the value that’s passed into the argument of the resolve function. We can also pass in an object with the then property where the value of it is the callback function of a promise. If the value has a then method, then the promise will be fulfilled with the value fulfilled by the then function. That is, the first parameter of the function for the value of the then function is the same as resolve, and the second parameter is the same as reject. For example, we can write the following:

Promise.resolve(1)  
  .then((value) => {  
    console.log(value);  
  })

Then we get 1 logged since 1 is the value we passed into the resolve function to return the promise with resolved value 1.

If we pass in an object with a then method inside, like in the following code:

Promise.resolve({  
    then(resolve, reject) {  
      resolve(1);  
    }  
  })  
  .then((value) => {  
    console.log(value);  
  })

Then we get value 1 logged. That’s because the Promise.resolve function will run the then function, and the resolve parameter for the function set to the then property will be assumed to be a function that’s called the resolve function in a promise. If we instead called the reject function inside the then function in the passed-in object, then we get a rejected promise, like in the following code:

Promise.resolve({  
    then(resolve, reject) {  
      reject('error');  
    }  
  })  
  .then((value) => {  
    console.log(value);  
  })  
  .catch((error) => {  
    console.log(error);  
  })

In the code above, we get error logged since the promise is rejected.


Async and Await

With async and await, we can shorten the promise code. Before async and await, we had to use the then function and we had to put callback functions as an argument of all of our then functions. This makes the code long as we have lots of promises. Instead, we can use the async and await syntax to replace the then function and its associated callbacks. For example, we can shorten the following code from:

const promise1 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(1), 2000);  
});

const promise2 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(2), 1000);  
});

promise1  
  .then((val1) => {  
    console.log(val1);  
    return promise2;  
  })  
  .then((val2) => {  
    console.log(val2);  
  })

to:

const promise1 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(1), 2000);  
});  

const promise2 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(2), 1000);  
});

(async () => {  
  const val1 = await promise1;  
  console.log(val1)  
  const val2 = await promise2;  
  console.log(val2)  
})()

We replaced the then and callbacks with await. Then we can assign the resolved values of each promise as variables. Note that if we use await for our promise code, then we have to put async in the function signature as we did in the above example. To catch errors, instead of chaining the catch function at the end, we use the catch clause. Also, instead of chaining the finally function at the bottom to run code when a promise ends, we use the finally clause after the catch clause.

For example, we can write:

const promise1 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(1), 2000);  
});  
const promise2 = new Promise((resolve, reject) => {  
  setTimeout(() => reject('error'), 1000);  
});

(async () => {  
  try {  
    const val1 = await promise1;  
    console.log(val1)  
    const val2 = await promise2;  
    console.log(val2)  
  } catch (error) {  
    console.log(error)  
  } finally {  
    console.log('finally runs');  
  }})()

In the code above, we got the resolved value of the promise assigned to a variable instead of getting the value in the callback of the then function, like in the const response = await promise1 line above. Also, we used the try...catch...finally blocks to catch the errors of rejected promises and the finally clause instead of the finally function with a callback passed in to create code that runs no matter what happens to the promises.

Like any other function that uses promises, async functions always return promises and cannot return anything else. In the example above, we showed that we can chain promises in a much shorter way than with the then function with callbacks passed in as an argument.


Conclusion

With promises, we can write asynchronous code with ease. Promises are objects that represent a process that runs for an indeterminate amount of time and may result in success or failure. To create promises in JavaScript, we use a Promise object that’s a constructor for creating promises.

The Promise constructor takes a function called the executor function that has the resolve and reject parameters. Both parameters are functions that we call to either fulfill the promise (which means end it successfully with some value) or to reject the promise (which means returning some error value and marking the promise as failed). The function’s return value is ignored. Therefore, promises can’t return anything other than promises.

Promises are chainable since promises return promises. The then function of promises can throw an error, return resolved values, or return other promises that are pending, fulfilled, or rejected.