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 thatGetData()
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 eachawait
statement, other code outside ofGetData()
is free to continue executing - that's what theasync
keyword tells us
- While the
- If an exception is thrown during any of the
await
functions, thetry...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; themode
option (CORS) will be pertinent to most requestsfetch()
always returns aresponse
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-usedDespite 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
orexport 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.