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
JavaScript Basics

Easily Iterate Over JavaScript Collections with the For-Of Loop

Starting with ES2015, we have a new kind of loop to loop over iterable objects. The new for…of loop is a new kind of loop that lets us…

Starting with ES2015, we have a new kind of loop to loop over iterable objects. The new for...of loop is a new kind of loop that lets us loop over any iterable objects without using a regular for loop, while loop, or using the forEach function in the case of arrays. It can be used directly to iterate through any iterable objects, which include built in objects like Strings, Arrays, array-like objects like arguments and NodeList , TypedArray , Map , Set and any user-defined iterables. User-defined iterables include entities like generators and iterators.

If we want to use the for...of loop to iterate over an iterable object, we can write it with the following syntax:

for (variable of iterable){
  // run code
}

The variable in the code above is the variable representing each entry of the iterable object that are being iterated over. It can be declared with const , let or var . The iterable is the object where the properties are being iterated over.

For example, we can use it to iterate over an array like in the following code:

const arr = [1,2,3];

for (const num of arr) {
  console.log(num);
}

The code above, the console.log statements will log 1, 2, and 3. We can replace const with let if we want to assign the variable we used for iteration in the for...of loop. For example, we can write:

const arr = [1,2,3];

for (let num of arr) {
  num *= 2 ;
  console.log(num);
}

The code above, the console.log statements will log 2, 4, and 6 since we used the let keyword to declare the num so we can modify num in place by multiplying each entry by 2. We cannot reassign with const so we have to use let or var to declare the variable we want to modify in each iteration.

We can also iterate over strings. If we do that we all get each character of the string in each iteration. For example, if we have the code below:

const str = 'string';

for (const char of str) {
  console.log(char);
}

Then we get the individual characters of 'string' logged in each line.

Likewise, we can iterate over TypedArrays, which contains binary data represented by a series of numbers in hexadecimal format. For example, we can write the following code:

const arr = new Uint8Array([0x00, 0x2f]);`

for (const num of arr) {
  console.log(num);
}`

In the example above, console.log will log 0 and 47. Note that the logged value is in decimal format but the entered value is in hexadecimal format.

If we iterate over Maps, then we get each entry of the Map. For example, we can write the following code to iterate over Maps:

const map = new Map([['a', 2], ['b', 4], ['c', 6]]);

for (const entry of map) {
  console.log(entry);
}

If we log the entries, we get ['a', 2], ['b', 4], and ['c', 6] . Maps consists of key-value pairs as their entries. When we iterate over a Map, we get the key as the first element and the value as the second element is each entry. To get the key and value of each entry into its own variable we can use the destructuring operator, like in the following code:

const map = new Map([['a', 2], ['b', 4], ['c', 6]]);

for (const [key, value] of map) {
  console.log(key, value);
}

Then when we log the entries, we get 'a' 2, 'b' 4, and 'c' 6 .

We can also use the for...of loop for Sets. For example, we can loop over a Set by doing the following:

const set = new Set([1, 1, 2, 3, 3, 4, 5, 5, 6]);

for (const value of set) {
  console.log(value);
}

We set that we get 1, 2, 3, 4, 5, and 6 logged since the Set constructor automatically eliminates duplicate entries by keeping the first occurrence a value in the Set and discarding the later occurrence of the same value.

The for...of loop also works for iterating over the arguments object, which is an global object that has the arguments that were passed into the function when the function is called. For example, if we write the following code:

(function() {
  for (const argument of arguments) {
    console.log(argument);
  }
})(1, 2, 3, 4, 5, 6);

We see that we see 1, 2, 3, 4, 5, and 6 logged since this is what we passed in when we called the function. Note that this only works for regular functions since the context of this has to be changed to the function being called instead of window . Arrow functions doesn’t change the content of this , so we won’t get the correct arguments when we run the same loop inside an arrow function.

Also, we can iterate over a list of DOM Node objects, called a NodeList . For example, is a browser implemented the NodeList.prototype[Symbol.iterator] , then we can use the for...of loop like in the following code:

const divs = document.querySelectorAll('div');

for (const div of divs) {
  console.log(div);
}

In the code above we logged all the div elements that are in the document.

With for...of loops, we can end the loop by using the break , throw or return statements. The iterator will close in this case, but the execution will continue outside the loop. For example, if we write:

function* foo(){
  yield 'a';
  yield 'b';
  yield 'c';
};

for (const o of foo()) {
  console.log(o);
  break;
}

console.log('finished');

In the code above, we only log ‘a’ because we have a break statement at the end of the for...of loop, so after the first iteration, the iterator will close and the loop ends.

We can loop over generators, which are special functions that returns a generator function. The generator function returns the next value of an iterable object. It’s used for letting us iterate through a collection of objects by using the generator function in a for...of loop.

We can also loop over a generator that generate infinite values. We can have an infinite loop inside the generator to keep returning new values. Because the yield statement doesn’t run until the next value is requested, we can keep an infinite loop running without crashing the browser. For example, we can write:

function* genNum() {
  let index = 0;
  while (true) {
    yield index += 2;
  }
}

const gen = genNum();
for (const num of gen) {
  console.log(num);
  if (num >= 1000) {
    break;
  }
}

If we run the code above, we see that we get numbers from 2 to 1000 logged. Then num is bigger than 1000, so that the break statement is ran. We cannot reuse the generator after it’s closed, so if we write something like the following:

function* genNum() {
  let index = 0;
  while (true) {
    yield index += 2;
  }
}

const gen = genNum();
for (const num of gen) {
  console.log(num);
  if (num >= 1000) {
    break;
  }
}

for (const num of gen) {
  console.log(num);
  if (num >= 2000) {
    break;
  }
}

The second loop won’t run because the iterator that was generated by the generator is already closed by the first loop with the break statement.

We can iterate over other iterable objects that have the method denoted with the Symbol.iterator Symbol defined. For example, if we have the following iterable object defined:

const numsIterable = {
  [Symbol.iterator]() {
    return {
      index: 0,
      next() {
        if (this.index < 10) {
          return {
            value: this.index++,
            done: false
          };
        }
        return {
          value: undefined,
          done: true
        };
      }
    };
  }
};

Then we can run the loop below to show log the generated results:

for (const value of numsIterable) {
  console.log(value);
}

When we run it, should see 0 to 9 logged when console.log is run in the loop above.

It’s important that we don’t confuse the for...of loop with the for...in loop. The for...in loop if for iterating over the top-level keys of objects including anything up the prototype chain, while the for...of loop can loop over any iterable object like arrays, Sets, Maps, the arguments object, the NodeList object, and any user-defined iterable objects.

For example, if we have something like:

Object.prototype.objProp = function() {};
Array.prototype.arrProp = function() {};

const arr = [1, 2, 3];
arr.foo = 'abc';

for (const x in arr) {
  console.log(x);
}

Then we get 0, 1, 2, ‘foo’, ‘arrProp’ and ‘objProp’ logged, which are keys of objects and methods that are defined for the arr object. It included all properties and methods up the prototype chain. It inherited all properties and methods from Object and Array that were added to Object and Array’s prototype so we get all the things in the chain inheritance in the for...in loop. Only enumerable properties are logged in the arr object in arbitrary order. It logs index and properties we defined in Object and Array like objProp and arrProp .

To only loop through properties that aren’t inheritance from an object’s prototype, we can use the hasOwnPropetty to check if the property is defined on the own object:

Object.prototype.objProp = function() {};
Array.prototype.arrProp = function() {};

const arr = [1, 2, 3];
arr.foo = 'abc';

for (const x in arr) {
  if (arr.hasOwnProperty(x)){
    console.log(x);
  }
}

objProp and arrProp are omitted because they’re they’re inherited from Object and Array objects respectively.

The for...of loop is a new kind of loop that lets us loop over any iterable objects without using a regular for loop, while loop, or using the forEach function in the case of arrays. It can be used directly to iterate through any iterable objects, which include built in objects like Strings, Arrays, array-like objects like arguments and NodeList , TypedArray , Map , Set and any user-defined iterables. User-defined iterables include entities like generators and iterators. This is a handy loop because it lets us over any iterable object rather than just arrays. Now we have a loop statement that works with iterable object.

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
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

Storing Key-Value Pairs With JavaScript Maps

With ES2015, we have a new data structure to store key-value pairs, called Maps, which are dictionaries that we can use to store any object as keys and any object as values.

Before we had Maps, we had to use objects as dictionaries that only use strings as keys, but values could be anything.

Using Maps is simple, we can define Maps like in the following code:

let messageMap = new Map();  
messageMap.set('hi', 'Hello');  
messageMap.set('greeting', 'How are you doing?');  
messageMap.set('bye', 'Bye');

Instead of using the set method to add our keys and values, we can also pass a nested array where each entry of the array has the key as the first element and the value as the second element.

For example, we can write:

const arrayOfKeysAndValues = [  
  ['a', 1],  
  ['b', 2],  
  ['c', 3],  
  ['d', 3],  
  ['e', 4],  
  ['f', 5],  
  ['g', 6],  
  ['h', 7],  
  ['i', 8],  
  ['j', 9],  
  ['k', 10],  
  ['l', 11],  
  ['m', 12],  
  ['n', 13],  
  ['o', 14],  
]  
let numMap = new Map(arrayOfKeysAndValues);  
console.log(numMap)

The code above will create a Map with the first element of each array as the key and the second element of each array as the corresponding value. So, after running the console.log line, we get all the entries of the Map listed in the same order as the array.

We can get the number of key-value pairs defined in a Map by using the size property:

messageMap.size // 3

To get the value by the key, we use the get function:

messageMap.get('hi'); // 'Hello'  
messageMap.get('hello'); // undefined

It returns the value if the key exists and undefined if an entry with the key doesn’t exist.

To remove an entry given the key, we can use the delete function with the key passed in:

messageMap.delete('hi');

If we call set with the same key passed in more than once, then the value in the later set call overwrites the earlier one. For example:

let messageMap = new Map();  
messageMap.set('hi', 'Hello');  
messageMap.set('greeting', 'How are you doing?');  
messageMap.set('bye', 'Bye');  
messageMap.set('hi', 'Hi');
console.log(messageMap.get('hi'))

If we run console.log(messageMap.get(‘hi’)), we get 'Hi' instead of 'Hello'.

To clear all the entries of a Map object, we can call the clear function, like in the following code:

let messageMap = new Map();  
messageMap.set('hi', 'Hello');  
messageMap.set('greeting', 'How are you doing?');  
messageMap.set('bye', 'Bye');  
messageMap.set('hi', 'Hi');  
messageMap.clear();  
console.log(messageMap)  
console.log(messageMap.size)

We see that messageMap should be empty when we log it and that its size should be 0.

The entries of a Map can be iterated over with a for...of loop. With the destructuring assignment of each entry, we can get the key and value of each entry with a for...of loop like in the following code:

let messageMap = new Map();  
messageMap.set('hi', 'Hello');  
messageMap.set('greeting', 'How are you doing?');  
messageMap.set('bye', 'Bye');

for (let [key, value] of messageMap) {  
  console.log(`${key} - ${value}`);  
}

Objects and Maps are different in multiple ways. The keys of objects can only be of the string type. In Maps, keys can be of any value.

We can get the size of a Map easily with the size property which isn’t available in objects. The iteration order of Maps is in the iteration order of the elements, while the iteration of the keys of an object isn’t guaranteed.

Object has a prototype, so there are default keys in an object, but not in a Map.

Maps are preferred is we want to store non-string keys, when keys are unknown until run time, and when all the keys are the same type and values are the same type which may or may not be the same type as the keys.

We can use the entries function to get the entries of a Map. For example, we can write:

let messageMap = new Map();  
messageMap.set('hi', 'Hello');  
messageMap.set('greeting', 'How are you doing?');  
messageMap.set('bye', 'Bye');

for (let [key, value] of messageMap.entries()) {  
  console.log(`${key} - ${value}`);  
}

To loop through the entries of the Map.

Keys are checked for equality by mostly following the rules of the triple equals operator, except that NaN is considered equal to itself and +0 and -0 are also considered equal. The algorithm is called the same-value-zero algorithm.

So, if we have:

const map = new Map();  
map.set(NaN, 'abc');

Then we run map.get(NaN), we get that 'abc' returned. Also, it’s important to note that we can use non-string values as keys like we did above. For example:

let messageMap = new Map();  
messageMap.set(1, 'Hello');  
messageMap.set(2, 'How are you doing?');  
messageMap.set(3, 'Bye');

If we call messageMap.get(1), we get 'Hello'.

However, since the keys are retrieved by checking with the triple equal operator with NaN considered equal to itself, we cannot get the value that you expect directly from object keys.

let messageMap = new Map();  
messageMap.set({ messageType: 'hi' }, 'Hello');  
messageMap.set({ messageType: 'greeting' }, 'How are you doing?');  
messageMap.set({ messageType: 'bye' }, 'Bye');  
const hi = messageMap.get({  
  messageType: 'hi'  
})
console.log(hi);

hi will be undefined as it doesn’t check the content of the object for equality.

We can use array methods on Map if we convert it to an array first and then convert it back to a Map. For example, if we want to multiply all the values in a Map by 2, then we can write the following:

let numMap = new Map();  
numMap.set('a', 1);  
numMap.set('b', 2);  
numMap.set('c', 3);

let multipliedBy2Array = [...numMap].map(([key, value]) => ([key, value * 2]));

let newMap = new Map(multipliedBy2Array);
console.log(newMap)

As we can see from the code above, we can use the spread operator to convert a Map to an Array directly by copying the values from the Map to the array.

Each entry consists of an array with the key as the first element and the value as the second element. Then we can call map on it since it’s an array. We can then multiply each value’s entry by 2, while keeping each entry’s array structure the same, with key first and value second.

Then we can pass it directly to the constructor of the Map object and we can see that the new Map object, newMap, has all the values multiplied by 2 while keeping the same keys as numMap.

Likewise, we can combine multiple Maps by first putting them in the same array with the spread operator and combining them into one, then passing it to the Map constructor to generate a new Map object.

For example, we can write:

let numMap1 = new Map();  
numMap1.set('a', 1);  
numMap1.set('b', 2);  
numMap1.set('c', 3);  
numMap1.set('d', 3);  
numMap1.set('e', 4);  
numMap1.set('f', 5);  
numMap1.set('g', 6);  
numMap1.set('h', 7);  
numMap1.set('i', 8);  
numMap1.set('j', 9);  
numMap1.set('k', 10);  
numMap1.set('l', 11);let numMap2 = new Map();  
numMap2.set('m', 1);  
numMap2.set('n', 2);  
numMap2.set('o', 3);  
numMap2.set('p', 3);  
numMap2.set('q', 4);  
numMap2.set('r', 5);  
numMap2.set('s', 6);  
numMap2.set('t', 7);  
numMap2.set('u', 8);  
numMap2.set('v', 9);  
numMap2.set('w', 10);  
numMap2.set('x', 11);let numMap3 = new Map();  
numMap3.set('y', 1);  
numMap3.set('z', 2);  
numMap3.set('foo', 3);  
numMap3.set('bar', 3);  
numMap3.set('baz', 4);

const combinedArray = [...numMap1, ...numMap2, ...numMap3];  
const combinedNumMap = new Map(combinedArray);

When we log combinedNumMap, we see that we have all the original keys and values intact, but they were combined into one Map.

If we have overlapping keys, then the one that’s inserted later overwrites the values that were inserted earlier. For example:

let numMap1 = new Map();  
numMap1.set('a', 1);  
numMap1.set('b', 2);  
numMap1.set('c', 3);  
numMap1.set('d', 3);  
numMap1.set('e', 4);  
numMap1.set('f', 5);  
numMap1.set('g', 6);  
numMap1.set('h', 7);  
numMap1.set('i', 8);  
numMap1.set('j', 9);  
numMap1.set('k', 10);  
numMap1.set('l', 11);

let numMap2 = new Map();  
numMap2.set('m', 1);  
numMap2.set('n', 2);  
numMap2.set('o', 3);  
numMap2.set('p', 3);  
numMap2.set('q', 4);  
numMap2.set('r', 5);  
numMap2.set('s', 6);  
numMap2.set('t', 7);  
numMap2.set('u', 8);  
numMap2.set('v', 9);  
numMap2.set('w', 10);  
numMap2.set('x', 11);let numMap3 = new Map();  
numMap3.set('y', 1);  
numMap3.set('z', 2);  
numMap3.set('a', 21);  
numMap3.set('b', 22);  
numMap3.set('c', 23);

const combinedArray = [...numMap1, ...numMap2, ...numMap3];  
const combinedNumMap = new Map(combinedArray);

When we log combinedNumMap, we see that we have most of the original keys and values intact, but they were combined into one Map. However, a now maps to 21, b now maps to 22, and c now maps to 23.

With Maps, we can have key-value pairs where the keys aren’t strings. Maps are collections that can be iterated in the order that the key-value pairs are inserted in.

They can be converted to arrays with each entry being an array with the key as the first element and the value as the second element, which means that we can convert them to arrays and then use arras operations like array methods.

We can use the spread operator to manipulate them in the way that we can do with any arrays, then convert them back to Maps again. We can use the set function to add more entries, and the get function to get the value of the given key.

The values are retrieved by the keys by the same-value-zero algorithm which means the keys are matched by the triple equals operator, with the exception that -0 and +0 are considered equal and NaN is considered equal to itself.