2022-04-02T10:18:17Z

The React Mega-Tutorial, Chapter 1: Modern JavaScript

The JavaScript language has evolved significantly in the last few years, but because browsers have been slow in adopting these changes a lot of people have not kept up with the language. React encourages developers to use modern JavaScript, so this chapter gives you an overview of the newest features of the language.

The complete course, including videos for every chapter is available to order from my courses site. Ebook and paperback versions of this course are also available from Amazon. Thank you for your support!

For your reference, here is the complete list of articles in this series:

ES5 vs. ES6

The JavaScript language specification is managed by ECMA, a non-profit organization that maintains a standardized version of this language known as ECMAScript.

You may have heard the terms "ES5" and "ES6" in the context of JavaScript language versions. These refer to the 5th and 6th editions of the ECMAScript standard respectively. The ES5 version of the language, which was released in 2009, is currently considered a baseline implementation, with wide support across desktop and mobile devices. ES6, released in 2015, introduces significant improvements over ES5, and remains backwards compatible with it. Since the release of ES6, ECMA has been making yearly revisions to the standard, which continue to improve and modernize the language. In many contexts, the "ES6" denomination is loosely used for all the improvements brought to the language after ES5, and not strictly those from the ES6 specification.

How can web browsers keep up with a language that evolves so rapidly? They actually can't and don't! Features that were introduced in ES6 and later updates to the standard aren't guaranteed to be implemented in all browsers. To avoid code failing to run due to missing language features, modern JavaScript frameworks rely on a technique called transpiling, which converts modern JavaScript source code into functionally equivalent ES5 code that runs everywhere. Thanks to transpiling, JavaScript developers don't have to worry about what parts of the JavaScript language browsers support.

Summary of Recent JavaScript Features

The code in this book is written in modern JavaScript. Assuming you are familiar with the ES5 version of the language, you can use the sections that follow to refresh your knowledge of the newer parts of the language and fill any gaps that you may have.

Semicolons

The rules regarding when semicolons are required in JavaScript are confusing. The JavaScript compiler assumes implicit semicolons in some situations, but not in others. In practice, this means that in most cases, semicolons do not need to be typed. What makes things complicated is that in some situations they are still required.

To avoid the confusion generated by the semicolon rules, for this book I have decided to use explicit semicolons after all statements. I do not use a semicolon after a closing } at the end of a function declaration or a control structure, such as a loop or a conditional. Why these exceptions? Most other languages that use the semicolons as statement separators do not require it after the closing brace.

Here are some examples:

const a = 1;  // <-- semicolon here

function f() {
  console.log('this is f');  // <-- semicolon here
}  // <-- but not here

An interesting situation occurs when an arrow function is assigned to a variable or constant. I consider this an exception to the exception rule above, so I use a semicolon in this case:

const f = () => {
  console.log('this is f');
};  // <-- this is an assignment, so a semicolon is used

It goes without saying that you do not need to adopt my semicolon choices if you don't like them. If you have developed a personal preference for using or omitting semicolons, you can definitely use it in place of my own.

Trailing Commas

When defining objects or arrays that span multiple lines, it is useful to leave a comma after the last element. Look at the following examples:

const myArray = [
  1,
  3,
  5,
];

const myObject = {
  name: 'susan',
  age: 20,
};

The commas after the last elements of the array and object might seem like a syntax error at first, but they are valid. In fact, JavaScript is not unique in allowing this, as most other languages support trailing commas as well.

There are two benefits that result from this practice. The most important one is that if you need to reorder the elements, you can just move the lines up and down, without worrying about having to fix the commas. The other one is that when you need to add an element at the end, you do not need to go up to the previous line to add the trailing comma.

Imports and Exports

If you are used to writing old-style JavaScript applications for the browser, you probably never needed to "import" functions or objects from other modules. You simply added <script> tags that loaded your dependencies, and that was enough to bring what you needed into the global scope, which is accessible to any JavaScript code running in the context of the current page.

Thanks to the tooling incorporated into modern JavaScript front end frameworks, applications can now use a much more sane dependency model that is based on imports and exports.

A JavaScript module that wants to make a function or variable available for other modules to use, can declare it as a default export. Let's say there is a cool.js module with myCoolFunction() inside. Here is how this module could be written:

export default function myCoolFunction() {
  console.log('this is cool!');
}

Any other module that wants to use the function can then import it:

import myCoolFunction from './cool';

In this import, ./cool is the path of the dependent module, relative to the location of the importing file. The path can navigate up or down the directory hierarchy as necessary. The .js extension can be included in the import filename, but it is optional.

When using default exports, the name of the exported symbol does not really matter. The importing module can use any name it likes. The next example is also valid:

import myReallyCoolFunction from './cool';

Importing from third-party libraries works similarly, but the import location uses the library name instead of a local path. For example, here is how to import the React object:

import React from 'react';

A module can have only one default export, but it can also export additional things. Here is an extension of the above cool.js module with a couple of exported constants (you'll learn more about constants in the next section):

export const PI = 3.14;
export const SQRT2 = 1.41;

export default function myCoolFunction() {
  console.log('this is cool!');
}

To import a non-default export, the imported symbol must be enclosed in { and } braces:

import { SQRT2 } from './cool';

This syntax also allows multiple imports in the same line:

import { SQRT2, PI } from './cool';

Default and non-default symbols can also be included together in a single import line:

import myCoolFunction, { SQRT2, PI } from './cool';

If you want to learn about imports and exports in more detail, consult the import and export sections in the JavaScript reference.

Variables and Constants

Older JavaScript versions were very sloppy in terms of how to declare variables. Starting with ES6, the let and const keywords are used for the declaration of variables and constants respectively. You may have seen the var keyword used to declare variables in older versions of JavaScript. The var keyword has some scoping quirks, so it is best to replace it with the more predictable let.

To define a variable, just prepend it with the let keyword:

let a;

It is also possible to declare a variable and give it an initial value at the same time:

let a = 1;

If an initial value isn't given, the variable is assigned the special value undefined.

A constant is a variable that can only be assigned a value when it is declared:

const c = 3;

console.log(c); // 3
c = 4;  // error

While it may look confusing, it is perfectly legal to create a constant and assign a mutable object to it. For example:

const d = [1, 2, 3];

d.push(4);  // allowed
console.log(d)  // [1, 2, 3, 4]

Why does this work? Because the requirement for constants is that they do not have a new assignment after declaration. There are no requirements regarding mutating the initially assigned value.

The JavaScript reference documentation contains more information about let and const.

Equality and Inequality Comparisons

Older JavaScript implementations had very strange rules in regard to automatic casting of values between different types. For that reason, the original equality (==) and inequality (!=) operators work in ways that may appear wrong, or at least different to what you would expect.

To avoid breaking older code, these comparison operators preserve the odd behaviors, but recent versions of JavaScript introduced new comparison operators === and !==, so that more predictable comparisons can be used.

In general, all equality and inequality comparisons should use the newer operators. Examples:

let a = 1;

console.log(a === 1);  // true
console.log(a === '1');  // false
console.log(a !== '1');  // true

Given that many other languages use the == and != operators for comparisons, it is very common to inadvertently use these when writing JavaScript. In a properly set up project (such as the one you will build with this book), static code analysis tools can detect and warn about this mistake.

See the Strict equality and Strict inequality operators in the JavaScript reference documentation for more details.

String Interpolation

Many times it is necessary to create a string that includes a mix of static text and variables. ES6 uses template literals for this:

const name = 'susan';
let greeting = `Hello, ${name}!`;  // "Hello, susan!"

There are more examples in the Template literals reference documentation.

For-Of Loops

Older versions of JavaScript only provide strange and contorted ways to iterate over an array of elements, but luckily ES6 introduces the for ... of statement for this purpose.

Given an array, a for-loop that iterates over its elements can be constructed as follows:

const allTheNames = ['susan', 'john', 'alice'];
for (name of allTheNames) {
  console.log(name);
}

Arrow Functions

ES6 introduces an alternative syntax for the definition of functions that is more concise, in addition to having a more consistent behavior for the this variable, compared to the function keyword.

Consider the following function, defined in the traditional way:

function mult(x, y) {
  const result = x * y;
  return result;
}

mult(2, 3);  // 6

Using the newer arrow function syntax, the function can be written as follows:

const mult = (x, y) => {
  const result = x * y;
  return result;
};

mult(2, 3);  // 6

Looking at this it isn't very clear why the arrow syntax is better, but this syntax can be simplified in a few ways. If the function has a single statement instead of two, then the curly braces and the return keyword can be omitted, and the entire function can be written in a single line:

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

If the function accepts a single argument instead of two, then the parenthesis can also be omitted:

const square = x => x * x;

square(2);  // 4

When passing a callback function as an argument to another function, the arrow function syntax is more convenient. Consider the following example, shown with traditional and arrow function definitions:

longTask(function (result) { console.log(result); });

longTask(result => console.log(result));

See the Arrow function documentation for more information.

Promises

A promise is a proxy object that is returned to the caller of an asynchronous operation running in the background. This object can be used by the caller to keep track of the background task and obtain a result from it when it completes.

The promise object has then() and catch() methods (among others) that allow the construction of chains of asynchronous operations with solid error handling.

Many internal and third-party JavaScript libraries return promises. Here is an example use of the fetch() function to make an HTTP request, and then print the status code of the response:

fetch('https://example.com').then(r => console.log(r.status));

This executes the HTTP request in the background. When the fetch operation completes, the arrow function passed as an argument to the then() method is invoked with the response object as an argument.

Promises can be chained. A common case that requires chaining is when making an HTTP request that returns a response with some data. The following example shows how the request operation is chained to a second background operation that reads and parses JSON data from the server response:

fetch('http://example.com/data.json')
  .then(r => r.json())
  .then(data => console.log(data));

This is still a single statement, but I have broken it up into multiple lines to increase clarity. Once the fetch() call completes, the callback function passed to the first then() executes with the response object as an argument. This callback function returns r.json(), a method of the response object that also returns a promise. The second then() call is invoked when the second promise completes, receiving the parsed JSON data as an argument.

To handle errors, the catch() method can be added to the chain:

fetch('http://example.com/data.json')
  .then(r => r.json())
  .then(data => console.log(data))
  .catch(error => console.log(`Error: ${error}`));

For additional details on promises, consult the Promise API documentation.

Async and Await

Promises are a nice improvement that help simplify the handling of asynchronous operations, but having to chain several actions in long sequences of then() calls can still generate code that is difficult to read and maintain.

In the 2017 revision of ECMAScript, the async and await keywords were introduced as an alternative way to work with promises. Here is the first fetch() example from the previous section once again:

fetch('http://example.com/data.json')
  .then(r => r.json())
  .then(data => console.log(data));

Using async/await syntax, this can be coded as follows:

async function f() {
  const r = await fetch('https://example.com/data.json');
  const data = await r.json();
  console.log(data);
}

With this syntax, the asynchronous tasks can be given sequentially, and the resulting code looks very close to how it would be with synchronous function calls. A limitation is that the await keyword can only be used inside functions declared with async.

Error handling in async functions can be implemented with try/catch:

async function f() {
  try {
    const r = await fetch('https://example.com/data.json');
    const data = await r.json();
    console.log(data);
  }
  catch (error) {
    console.log(`Error: ${error}`);
  }
}

An interesting feature of functions declared as async is that they are automatically upgraded to return a promise. The f() function above can be chained to additional asynchronous tasks using the then() method if desired:

f().then(() => console.log('done!'));

Or of course, it can also be awaited if the calling function is also async:

async function g() {
  await f();
  console.log('done!');
}

The arrow function syntax can also be used with async functions:

const g = async () => {
  await f();
  console.log('done!');
};

The Making asynchronous programming easier with async and await section of the JavaScript documentation is a good place to learn more.

Spread Operator

The spread operator (...) can be used to expand an array or object in place. This allows for very concise expressions when working with arrays or objects. The best way to learn the spread operator is through some examples.

Let's say you have an array with some numbers, and you want to find the smallest of them. The traditional way to do this would require a for-loop. With the spread operator, you can leverage the Math.min() function, which takes a variable list of arguments:

const a = [5, 3, 9, 2, 7];
console.log(Math.min(...a));  // 2

The basic idea is that the ...a expression expands the contents of a, so the Math.min() function receives five independent arguments instead of single array argument.

The spread operator can also be used to create a new array by mixing another array with new elements:

const a = [5, 3, 9, 2, 7];
const b = [10, ...a, 8, 0];  // [10, 5, 3, 9, 2, 7, 8, 0]

It also allows for a simple way to do a shallow copy of an array:

const c = [...a];  // [5, 3, 9, 2, 7]

The spread syntax also works with objects:

const d = {name: 'susan'};
const e = {...d, age: 20};  // {name: 'susan', age: 20}
const f = {...d};  // {name: 'susan'}

An interesting usage of the spread operator on objects is to make partial updates:

const user = {name: 'susan', age: 20};
const new_user = {...user, age: 21};  // {name: 'susan', age: 21}

Here, the collision that occurs when having two values for the age key is resolved by using the version that appears last.

See the Spread syntax reference for more details.

Object Property Shorthand

In the same league as the spread operator, the object property shorthand provides a simplified syntax for object properties. Consider how the following object is created:

const name = 'susan';
const age = 20;
const user = {name: name, age: age};

Do you see the repetition of name and age? These keywords are used as property names, and also as the names of the constants that hold the property values. With the object property shorthand syntax, you can create the same object as follows:

const user = {name, age};

Objects created as above use the name of the given variable or constant as the property name, and also assign the value to the new property.

Shorthand and regular properties can be combined as well:

const user = {name, age, active: true};  // {name: 'susan', age: 20, active: true}

Destructuring Assignments

Destructuring assignments are yet another nice syntax shorthand that can be used to simplify assignments of arrays and objects. The idea is that the right side value can be decomposed into its elements on the fly as part of the assignment operation. Here is an array example:

const a = ['susan', 20];
let name, age;
[name, age] = a;

The square brackets on the left side of the assignment tell JavaScript that the right side is an array that must be taken apart before assigning the elements to the list of variables.

What happens if the number of elements in the left and right sides don't match? If the left side has more elements than the right side, then the extra elements on the left are assigned the undefined value. If the right side has more elements than the left side, then the extra elements are discarded.

There is an interesting combination between the destructuring assignment and the spread operator discussed above. Consider the following example:

const b = [1, 2, 3, 4, 5];
let c, d, e;
[c, d, ...e] = b;
console.log(c);  // 1
console.log(d);  // 2
console.log(e);  // [3, 4, 5]

Destructuring assignments can also be used with objects:

const user = {name: 'susan', active: true, age: 20};
const {name, age} = user;
console.log(name);  // susan
console.log(age);  // 20

This technique can be applied not only to direct assignments, but also to function arguments. The following example demonstrates it:

const f = ({ name, age }) => {
  console.log(name);  // susan
  console.log(age);  // 20
};

const user = {name: 'susan', active: true, age: 20};
f(user);

Here the f() arrow function accepts an object as its only argument, but instead of accepting the whole object, the function just takes the name and age properties from the input. As with assignments, if the object has additional properties, they are discarded, and if any of the named properties in the function declaration are not in the object, then they are assigned the undefined value.

To learn about more ways to use this feature consult the Destructuring Assignment section of the JavaScript reference.

Classes

A big omission in the earlier versions of the JavaScript language up to, and including ES5 is classes, which are the core component of object-oriented programming. Below you can see an example of an ES6-style class:

class User {
  constructor(name, age, active) {  // constructor
    this.name = name;
    this.age = age;
    this.active = active;
  }

  isActive() {  // standard method
    return this.active;
  }

  async read() {  // async method
    const r = await fetch(`https://example.org/user/${this.name}`);
    const data = await r.json();
    return data;
  }
}

To create an instance of a class, the new keyword is used:

const user = new User('susan', 20, true);

Learn more about classes in the JavaScript reference.

JSX

The last modern JavaScript feature discussed in this chapter is in a category of its own, as it is not part of any ECMAScript specification, and it is not intended to ever be. It is called JSX, which is short for JavaScript XML. Its purpose is to make it easier to create inline structured content, mainly to be used in HTML pages.

Let's say that you need to create an HTML paragraph element. Using plain JavaScript and the DOM API, you could create this <p> element as follows:

const paragraph = document.createElement('p');
paragraph.innerText = 'Hello, world!';

Can you imagine what it would be like to create a more complex tree of elements, like maybe a complete table, using plain JavaScript? With JSX, it becomes much easier, and the resulting code is more readable:

const paragraph = <p>Hello, world!</p>;

Here is a more complex example of an HTML table:

const myTable = (
  <table>
    <tr>
      <th>Name</th>
      <th>Age</th>
    </tr>
    <tr>
      <td>Susan</td>
      <td>20</td>
    </tr>
    <tr>
      <td>John</td>
      <td>45</td>
    </tr>
  </table>
);

The JSX syntax is a key component of React applications. While technically not required by React, it makes HTML content and templates much easier to create and maintain.

Ready to continue? Here is Chapter 2!

10 comments

  • #1 Russ said 2022-04-03T11:33:13Z

    If I purchase the course, do I get the documents all at once, now? Basically, is the series ready, and just being released one at a time.

    I followed your flask series (all versions) from the very beginning, and since I am getting ready to rejoin the workforce, this seems like an ideal way to refamiliar myself with the latest ES, as well as React. I cannot say enough about the flask tutorials; Top notch in every respect (for my learning style, anyway).

  • #2 Scott said 2022-04-03T18:09:20Z

    Thanks for this post! I've read pretty much all of this content in other forms over the years trying to get my head around it (I come from Python), but it's never been as clear as it is when you write it. Bookmarked!

  • #3 Miguel Grinberg said 2022-04-04T14:26:55Z

    @Russ: Yes, but the content isn't ready yet. You are "pre-ordering" the course. The expected availability for the written course is before the end of this month. The video and ebook content are going to be available before end of May.

  • #4 Kenneth said 2022-04-06T05:39:10Z

    Thanks, Miguel. This was the best summary of the JS features I have read. I think React will be my summer project, just to have fun and follow your excellent tutorials.

  • #5 Arthur said 2022-04-06T17:40:49Z

    Hello Miguel,

    First of all, super excited for this. Will you be covering Redux in this mega-tutorial?

  • #6 Miguel Grinberg said 2022-04-06T23:14:23Z

    @Arthur: No. I will be using React contexts.

  • #7 Roger said 2022-04-07T04:00:41Z

    Thanks Miguel, will this tutorial introduce React hooks use cases instead of some classes?

  • #8 Miguel Grinberg said 2022-04-07T22:06:22Z

    @Roger: this tutorial uses React hooks exclusively. Class-based components are not used at all.

  • #9 Arturo said 2022-04-11T14:08:47Z

    Hi Miguel,

    Awesome content! The backend that is going to be used in this course is designed with Flask?

  • #10 Miguel Grinberg said 2022-04-11T15:29:59Z

Leave a Comment