Crash Course for Composable, Part 2: Advanced JavaScript

Author

Brandon Bruno

Published

June 13, 2022

Tags

Crash Course for Composable, Part 2: Advanced JavaScript

In Part 1 of the Crash Course for Composable, we covered a few basic JavaScript concepts that modern web developers should understand at a high level.

Next, let's look at some more advanced JavaScript topics that are readily available to developers in both client and backend environments.

Promises

A JavaScript promise is the guarantee that some code will eventually execute, typically either via a success or error condition. Conceptually:

Promises are asynchronous in JavaScript. In the above diagram, the first block of code executes without a clear idea of when it will be complete. One of the two code blocks below will be executed later.

Promises use thenables to handle the flow of execution. Thenables use the special function then() to chain functions together. In plain English: "Do something, then do something else, then do something else..." and so on.

A simple example of a Promise being used:

GetDataFromServer()
    .then(data => bindToControls(data))
    .catch(error => console.log(error));

GetDataFromServer() does something asynchronously, and when it completes, it will succeed and call then() or fail and call the catch() method. Thenables aren't limited to just one of each method; multiple functions can be chained together:

GetDataFromServer()
	.then(results => processResults(results))
    .then(data => bindToControls(data))
	.then(() => updateUI())
    .catch(error => console.log(error));

What constitutes a success or failure is entirely up to the promise itself. In order to understand this concept, we need to look at the implementation of a promise function. A Promise object commonly accepts a function with two parameters: resolve and reject, which represent the success and failure conditions. It's up to the promise to determine when to call resolve() or reject(), as seen here:

let GetDataFromServer = new Promise((resolve, reject) => { 

	// Do some significant work asynchronously
	let status = sendAJAXRequest("https://myserver.com/getData");

	// Determine if this promise should succeed or fail
	if (status == 201) {
        resolve(); // Maps to .then()
    } else {
        reject();  // Maps to .catch()
    }
});

Promises are a big, deep, complicated JavaScript topic, so I recommend continued reading.

Promises: async/await

The async and await keywords are simply an alternative to promise chaining with then/catch when working with promises.

Promise chaining can get out of control and become difficult to parse in some situations:

GetDataFromServer()
	.then((response) => {

		if (response.status == 201) {

			processResults(response.payload)
				.then(data => bindToControls(data)
                              .then(() => updateUI()))
				.catch(error => alert('Something bad happened!'));
		}

	})
	.catch(error)=> console.log(error));

There are multiple nested then() functions doing different things for different execution paths (not to mention multiple places to handle errors). As requirements become more complex, this code might get very unwieldly.

async and await allow promises to be written without chaining. This code looks a lot more like traditional procedural code:

async function GetData() {

	try {

		let response = await GetDataFromServer();
		if (response.status == 201) {

			let data = await processResults(response.payload);
			await bindToControls(data);
			updateUI();
		}

	} catch (exception)	{

		console.log(`Error! ${exception}`);

	}

}

So what's happening here?

  • The async keyword tells the JavaScript interpreter that GetData() will be handling asynchronous code (so don't wait up for it)
  • Whenever a function with await is called, execution halts at that line until the promise resolves
    • While the GetData() function is paused at each await statement, other code outside of GetData() is free to continue executing - that's what the async keyword tells us
  • If an exception is thrown during any of the await functions, the try...catch block will take over and handle the first exception thrown

Again, promises are a big topic, so I recommend further reading on the async/await syntax.

Fetch

fetch() is a widely-supported and native way to handle client-server requests and resource fetching (goodbye jQuery .ajax() and XMLHttpRequest). The fetch() function is a Promise, so make sure you're good with the above concepts.

Keep in mind that fetch is available on the window object in browsers, so it's available everywhere in most client-side code. Here's an example of fetching a resource:

async function GetData()
{
	try
	{
		let response = await fetch("http://testdata.local/json.json");
		let data = await response.json();
		return data;
	}
	catch (exception)
	{
		console.log(exception);
	}
}

Fetch is pretty straightforward: point to a URL, do something (typically a GET or POST), and parse the response. Things to keep in mind with fetch:

  • The fetch() method accepts a URL and an object literal of parameters; the mode option (CORS) will be pertinent to most requests

  • fetch() always returns a response object that itself has several properties and functions (including HTTP status codes, the data payload, etc.)

  • The response object has specific functions for parsing fetched data/content: blob(), json(), text() are the most commonly-used

  • Despite its name, fetch() also supports POST and other send-type operations

The React docs recommend using fetch() for AJAX requests, hence why it's covered here. The MDN docs has a great write-up on using fetch.

Modules

Finally - and probably most important to React - are JavaScript modules. The component-based nature of React lends itself well to being modularized.

JavaScript has a few different types of module systems, and all do essentially the same thing: allow JavaScript code to be split across multiple files. Module systems have been supported by backend tooling (Node, Webpack, etc.) for a while, but browsers today also support modules (specifically ECMAScript Modules - or "ESM"). The examples that follow are ESM.

A module exports functionality - be it one or more functions, objects, classes, etc. For example, here is a file named person.js:

function sayHello() {
	console.log(`Hello, everyone!`);
}

function sayGoodbye() {
	console.log("Goodbye.");
}

export { sayHello, sayGoodbye }

Another file elsewhere can import one or more functions, objects, classes, etc. and then use them:

import { sayHello, sayGoodbye } from "./person.js";

sayHello();
sayGoodbye();

A couple things to note:

  • Exporting and importing explicit features is done with a comma-separated list, as seen above; these are named exports

  • Using export default in your module enables default exports - this means only one feature is exported and can be used elsewhere; example:

      export default function sayGoodMorning() {
      	console.log("Good morning!");
      }
    
  • A default export can be consumed elsewhere without curly braces; example:

      import sayGoodMorning from './person.js'
    
  • You can place the export or export default keywords before a function, object, class, etc. to export just that single item; this is useful when your module (or React component) consists of just one logical feature

The MDN docs for JavaScript modules has these features covered in more detail.

Additional Resources

Do you have questions, comments, or corrections for this post? Find me on Twitter: @BrandonMBruno