Adapting Your Code to ESNext Patterns

Adapting Your Code to ESNext Patterns

Written by Geoff Davis and edited by Solomon Osadolo

There is nothing quite like the start of a new year to really dive into new and exciting information; just as time presses on around us, Javascript has pressed on, constantly latching onto a horde of devices, and for some it is hard to really keep up. ES2017 is the latest and greatest edition of the ECMAScript specification – the checklist which determines what Javascript language features browsers should support in their respective engines – and builds on the strong foundations that ES2016 and ES2015 laid out.

With ES2017 and the power of transpilers such as Babel and Typescript, modern Javascript can be written with the most recent features, transformed, and then shipped as backwards-compatible code. This is invaluable as it allows code to be "future-proof", so-to-speak. This is great for new applications, side projects, and open source tools that developers will be or are currently building, but what about that legacy code in that corner of the computer that rarely sees daylight?

That is where this guide comes in; the following sections will outline some new features from ES2015-ES2017, and detail how to adapt some familiar patterns currently used in frontend Javascript development to modern patterns.

Async/Await

Do Javascript developers dream of asynchronous code? Perhaps. Regardless, none need dream anymore, for the async and await keywords have arrived in ES2017. These work in tandem to provide asynchronous behavior to functions. (Including class methods!)

async keyword

Asynchronous functions are denoted by placing the async keyword before the function expression. This includes IIFE functions and class methods.

// Function definition
async function async1() {
  // ...code
}

// Function assignment
const async2 = async () => {
  // ...code
}

// Immediately invoked function execution (IIFE)
(async async3() => {
  // ...code
})()

// Class method
class Test {
  async asyncMethod() {
    // ...code
  }
}

Using this keyword will transform the return value of the function into a Promise, with any value returned assigned to the PromiseValue.

await keyword

Inside an asynchronous function, use the await keyword to halt all other Javascript execution until the await-ed expression is complete. If this expression is Promise-based, the assigned variable(s) assume the value of the fulfilled Promise, otherwise the variable(s) assume the value of the expression as a resolved Promise.

// await Promise-based function
async function async4() {
  const data = await fetch('http://some.endpoint.com/');
  console.log(data.json()); // -> { foo: 'bar' }
}

// await simple mathematical expression
async function async5() {
  const four = await (2 + 2);
  console.log(four); // -> 4
}

// await a single value
async function async6() {
  const helloWorld = await 'Hello World';
  console.log(helloWorld); // -> 'Hello World'
}

Adapting to Async/Await

To adapt any function to be used in the async/await pattern, there are two approaches one can take - 1. Returning a Promise or 2. Returning an implicit Promise.resolve call. For demonstrating these two approaches, consider the following functions:

function requestXHR(url, handler, options) {
    options = options ? options : { method: "GET" };
    var xhr = new XMLHttpRequest();
    xhr.addEventListener("readystatechange", function() {
        if (this.readyState == 4 && this.status == 200) {
            handler(JSON.parse(xhr.responseText));
        }
    });
    xhr.open(options.method, url);
    xhr.send();
};

function square(n) {
  return n * n;
}

Promise Approach

This approach may be more suited to more complex functions, such as one that does have actual asynchronous functionality. It is useful because the function body executes inside a returned Promise body, using the resolve function to return the result of the function body, and using the reject function to return a fallback value or error message if the function body throws an error.

This allows more refined error-handling for functions that may need it.

function requestXHR(url, options = { method: 'GET' }) {
  return new Promise((resolve,reject) => {
    const xhr = new XMLHttpRequest();
    xhr.addEventListener("readystatechange", function() {
      if (this.readyState == 4 && this.status == 200) {
        resolve(JSON.parse(xhr.responseText));
      } else if (this.readyState == 4 && this.states != 200) {
        reject({ error: true })
      }
    });
    xhr.open(options.method, url);
    xhr.send();
  })
};

(async function async7() {
  const data = await requestXHR('https://api.service.com/');
  console.log(data); // -> { foo: 'bar' }
    
  const badData = await requestXHR('https://bad.notyourservice.com/')
  console.log(badData); // -> Uncaught (in promise) {error: true}
})

Implicit Approach

Using the implicit approach, the function’s returning value is implicitly transformed using a Promise.resolve call into a resolved Promise, with the function's result as the PromiseValue. If desired, the value could be explicitly wrapped in a call to the resolve method to increase readability.

function squareWithResolve(n) {
  return Promise.resolve(n * n);
}

(async function async8() {
  const four = await square(2);
  console.log(four); // -> 4

  const sixteen = await squareWithResolve(4);
  console.log(sixteen); // -> 16
})

Classes

Until ES2015, Javascript has not had an implementation of a class system, which is not surprising, seeing as the language is written to have prototype-based inheritance. However, this changed when the class structure was introduced. While technically syntactic-sugar, Javascript classes allow cleaner and safer extendability of the function-based "classes" that existed before ES2015.

Classes have become very popular with several prominent Javascript frameworks and libraries including Angular and React.

The following code demonstrates how an ES2015 class is structured and instantiated:

class MyClass {
  constructor(greeting) {
    this.className = 'MyClass';
    this.greeting = greeting;
    console.log('MyClass instance created');
  }
  static getClassName() {
    return this.className;
  }
  greet() {
    alert(this.greeting);
  }
}

const MyClassInstance = new MyClass('Hello World'); 
// log: MyClass instance created

Constructor

The class constructor is a function that describes the properties and executes any code upon creation of the class instance. The function can take arguments, allowing instances to assign unique values to class properties.

Note that any property declared here will be accessible from the class' instance.

constructor(greeting) {
  this.className = 'MyClass';
  this.greeting = greeting;
  console.log('class instance created');
}

Properties

Properties, as explained above, are declared and initialized in the class constructor function. They can also be declared, assigned, and even reassigned on the fly.

this.greeting = greeting;
this.currentYear = 2017;

console.log(MyClassInstance.greeting); // -> Hello World
console.log(MyClassInstance.currentYear); // -> 2017

// Oops! Fixing the year
this.currentYear = 2018;

console.log(MyClassInstance.currentYear); // -> 2018

Methods

Methods are functions that are accessible from the class instance. Often, methods will interact with or access class properties. To do this, call the property using this.propertyName, and methods will have access to their instance's value of the class property.

greet() {
  alert(this.greeting);
}
// Methods can be prefixed by the static keyword, attaching them directly to the class and not callable from instances of the class.
static getClassName() {
  return this.className;
}

MyClass.getClassName(); // -> "MyClass"
MyClassInstance.getClassName(); // -> Uncaught TypeError: MyClassInstance.getClassName is not a function
With support for async/await, class methods are able to be made asynchronous as well, since they are functions after all!
async greet() {
  return this.greeting
}

(async () => {
  const greeting = await MyClassInstance.greet();
  console.log(greeting); // -> Hello World
})()

Adapting to Classes

To adapt a pre-ES2015 function-based "class" implementation to use the class structure, move all initialized properties into a constructor method, and move all methods out to their own expression definitions. Consider the following function-based class:

function OldUser(uuid, first, last, dob) {
  this.uuid = uuid;
  this.first = first;
  this.last = last;
  this.fullName = first + " " + last;
  this.dob = new Date(dob);

  this.getAge = function getAge() {
    var now = new Date(Date.now());
    return now.getFullYear() - this.dob.getFullYear();
  };

  return this;
};

By wrapping the code in a class definition, moving the initialized properties and their assigning parameters into the constructor, and extracting methods to the class' top level, the function "class" is successfully migrated to the modern class structure.

class User {
  constructor(uuid, first, last, dob) {
    this.uuid = uuid;
    this.first = first;
    this.last = last;
    this.fullName = first + " " + last;
    this.dob = new Date(dob);
  }
  getAge() {
    const now = new Date(Date.now());
    return now.getFullYear() - this.dob.getFullYear();
  }
}

Default Arguments

Problems arise when functions that need parameters are not provided the appropriate amount of arguments. In the past, ternary operators or a combination of the AND/OR operators helped developers fill in the gaps when some arguments were left out, but were still necessary for the function to successfully execute. Consider the following functions:

function numberArguments(first, second) {
  first = first || 1;
  second = second ? second : 1;
  return first + second;
}

function getUserPermissions(username, signInRequired, notifications) {
  signInRequired = signInRequired || true;
  notifications = notifications || { browser: false, email: false, sms: false };
  if (signInRequired) {
    signIn();
  }
  return { 
    username: username,
    signInRequired: signInRequired,
    notifications: notifications
  };
}

Using ternary or the AND/OR operators works fine for string parameters. However, if the function parameters expect false, 0, or another "falsy" value, these operators will read the value and use the fallback, even though the fallbacks are meant to be used only if a value is not present.

// uses the default 1 for the first parameter, despite an argument provided
numberArguments(0,50); // -> 51

// calls signIn in the function body, even though signInRequired is false
getUserPermissions('johndoe', false, { browser: false, email: true, sms: true }); 
// -> { username: 'johndoe', signInRequired: true, notifications: { browser: false, email: true, sms: true } }

Adapting to Default Arguments

To remedy the faults of this pattern, use the new default arguments feature from ES2015; instead of defining a fallback value in the function body, assign the default argument value inside the argument list using the = operator.

const numberArguments = (first = 1, second = 1) => first + second;

numberArguments(0,50); // -> 50

const getUserPermissions = (username, signInRequired = true, notifications = { browser: false, email: false, sms: false }) => {
  if (signInRequired) {
    signIn();
  }
  return { 
    username,
    signInRequired,
    notifications
  };
}

getUserPermissions('johndoe', false, { browser: false, email: true, sms: true }); // -> { username: 'johndoe', signInRequired: false, notifications: { browser: false, email: true, sms: true } }

Destructuring

When working with objects and arrays, sometimes only one or two values are needed. Pre-ES2015, this would have meant selecting the individual properties (for objects) or elements (for arrays) from the respective variable.

var colors = ['red','green','blue'];

var red = colors[0];
var blue = colors[colors.indexOf('blue')];

var johnDoe = {
  username: 'johndoe',
  age: 27,
  site: 'https://johndoe.com',
  favoriteColors: ['red','green','blue']
};

var username = johnDoe.username; // -> johndoe
var site = johnDoe.site; // -> https://johndoe.com

Adapting to Destructuring

With ES2015's destructuring feature, individual properties/elements can be extracted directly from the object/array variable itself.

To do this, destructured values are declared using a variable assignment expression: use a variable declaration keyword (const, let, or var) and wrap the desired properties/elements to be extracted on the left hand side of the assignment with the respective structure delimiters ({} for objects, [] for arrays). On the right hand side should be a variable that holds elements/properties to be destructured.

const colors = ['red','green','blue'];
const johnDoe = {
  username: 'johndoe',
  site: 'https://johndoe.com',
  age: 27,
  favoriteColors: ['red','green','blue']
};

const [ red, green, blue ] = colors;
// red -> 'red', green -> 'green', blue -> 'blue'
const { username, age } = johnDoe;
// username -> 'johndoe', age -> 27

With objects, properties extracted using destructuring are identified by their keys; with arrays, elements are extracted using their position in the array, and are identified by variable identifiers, as if they are standard variable declarations.

Object and array destructuring can even be combined with default arguments to setup fallback values for properties/elements that may not be present in the object/array; this is useful when using right-hand expressions like data fetching or other potentially "unpredictable" values.

const [ red, green, blue, yellow = 'yellow' ] = colors;
// red -> 'red', green -> 'green', blue -> 'blue', yellow -> 'yellow'
const { username, age, email = 'johndoe@email.com' } = johnDoe;
// username -> 'johndoe', age -> 27, email -> 'johndoe@email.com'

Wrapping Up

The arrival of ES2015 heralded a refreshing renaissance of features and exciting developments in Javascript tooling. With the latest release of ES2017, a number of features are already available to replace and bulletproof old patterns whilst yielding cleaner and forward-thinking code.

Instacart, React, Dynamic Themes, and Inline Styles

Instacart, React, Dynamic Themes, and Inline Styles

I Dream of Node Streams

I Dream of Node Streams