-
Notifications
You must be signed in to change notification settings - Fork 10
Lesson 1.7
JavaScript is synchronous in nature. This means JavaScript runs code in the sequence they’re written.
Asynchronous is the opposite of synchronous.
Asynchronous code will not be executed immediately. They will only be executed after a certain event occurs.
One example is when we click a button.
const button = document.querySelector('button')
button.addEventListener('click', function (event) {
console.log('Button clicked')
})
The asynchronous part happens within addEventListener. The callback doesn’t run unless a click event occurs. This means JavaScript has effectively waited for something to happen. This wait is asynchronous — it delayed the code from being executed immediately.
Callbacks are a fundamental concept in asynchronous JavaScript.
Another example is executing a function after a certain amount of time has passed. We can do this with setTimeout.
setTimeout(function () {
console.log('1 second has passed')
}, 1000)
API stands for Application Programming Interface. It is code that allows you to “talk” to other programs through the code you write. Other programs in this case refers to code that others have written.
For example, the code above using document.querySelector that lets you select an element is one small part of the DOM API.
AJAX (or Ajax) means Asynchronous JavaScript and XML. When people say AJAX, they normally refer to the process of fetching a resource.
- Send a request to a server
- Wait for a response to come back
The term AJAX was used in the past because the responses will come back in an XML form. The term AJAX is outdated since we don’t use XML anymore, we use JSON now.
JSON stands for JavaScript Object Notation.
We use JSON as the main format to pass information between browsers and servers today. A JSON Object looks like a JavaScript object. The major difference between JSON and JavaScript is:
- JSON can only contain strings
- JSON property and value pairs are always wrapped with double quotes
{
"firstName": “Sam”,
"lastName": “Filip”,
"occupation": "engineer",
"age": "35"
}
There are two ways to fetch resources.
- Using XMLHTTPRequest (XHR for short)
- Using the Fetch API
Both XHR and Fetch are well supported on all browsers. The major difference is that XHR uses callbacks while Fetch uses Promises. Fetch is the standard practice today in fetching resources on the web.
The Fetch API is a newer method that lets you perform Ajax.
To send a request with Fetch:
fetch(url, options)
- url is the address you want to request information from.
- options is a JavaScript object that lets you provide extra options.
To see the response from Fetch, you need to write a then statement.
fetch('https://link-to-resource')
.then(response => console.log(response))
Before we can use the data in the response variable, we need to convert this readable stream into JavaScript before we can use it. This is done with response.json that comes with a Fetch response.
Then you get the payload in the next then method.
fetch('https://link-to-resource')
.then(response => response.json())
.then(data => console.log(data))
A JavaScript Promise is an if-else statement for the future. We don’t know whether the statement will flow into if or else until the future arrives. So promises have three states:
- Pending
- Resolved (or fulfilled)
- Rejected
All promises have the then and catch methods.
- We act on resolved promises in then.
- We can act on rejected promises in catch.
If you return a value from a then call, you can use that value in the next then call. response.json in the Fetch API uses this feature. Remember, a Fetch request is a promise.
fetch('https://link-to-resource')
.then(response => response.json())
.then(data => {/* do something with data */}
If an error occurs in a promise chain, the error will be passed directly into the next catch call. It will skip any then calls along the way.
This means we can handle all errors with a single catch call:
promise
.then(face => new Error('new error’)) // Throws an error
.then(face => console.log(‘something…’)) // Skipped
.catch(face => console.log(‘error’)) // Jumped here
All promises have a finally method. This method will be called after all then and catch calls. It is optional and not always needed and mainly used for cleanup.
promise.then(/*…*/).catch(/*…*/).finally(/*…*/)
The purpose of async / await is to make complicated asynchronous code easier to understand. It does this by making asynchronous code look synchronous.
You declare asynchronous functions with an async keyword. The async keyword must be placed before the function keyword.
async function functionName(arguments) {
// Do something asynchronous
}
You can also create asynchronous functions with the arrow function syntax. In this case, you write async before the arguments.
const functionName = async arguments => {
// Do something asynchronous
}
You call asynchronous functions the same way you call normal functions.
// Calling an async function
anAsyncFunction()
Asynchronous functions use promises underneath the hood so they always return a promise. The await keyword lets you wait for a promise to resolve (or reject) before proceeding to the next line in the code.
The await keyword must be written inside an asynchronous function.
async function getSomething() {
await fetch('https://resource-link')
}
You can use a try / catch block to check whether the promise resolves or rejects. This is very similar to then and catch calls within a promise.
async function getSomething() {
try {
await fetch('https://resource-link')
// Promise resolves.
} catch (error) {
// Promise rejects.
}
}
You don’t have to nest all await calls inside a try / catch block. When we call the async function, we can attach a catch block to handle all errors.
async function getSomething() {
const response = await fetch('https://resource-link')
const data = await response.json()
// Do something with data
}
getSomething().catch(error => {
/* Handle the error */
})
- Open
src/App.jsx - Delete the
useSemiPersistentStatefunction call fromApp - Copy the
useStateanduseEffecthooks fromuseSemiPersistentStatefunction back intoApp - Delete the
useSemiPersistentStatefunction - Run your application and view in browser
- Verify that your Todo List still appears correctly
Currently our list data is retrieved synchronously from the browser's storage, but in the next lesson we will be fetching it asynchronously from an API. Let's update our code to mimic asynchronous data fetching:
- Below the
todoListstate, define auseEffectReact hook with an empty dependency list - Inside the side-effect handler function, define a new Promise and pass in a callback function with parameters
resolveandreject- hint:
Promise()constructor
- hint:
- To mimic a loading delay, inside the callback function declare a timeout (hint:
setTimeoutmethod) with the following arguments:- callback: function with no parameters
- delay time: 2000 milliseconds (2 seconds)
- Inside the timeout callback function, call the parameter
resolvewhich is a callback function for when the Promise is successful and pass it an Object with propertydataas a nested empty Object - Inside the
dataobject, add a propertytodoListand set it's value to the initial/default list state (copy fromuseStatehook) - Update the default state for
todoListto be an empty Array - View your application in the browser
- Notice that the Todo List is now empty and doesn't persist after refresh
So what's missing? We are retrieving our list from localStorage but we aren't updating our todoList state with the data so it remains empty. Let's fix that:
- Chain a
thenmethod to yourPromiseconstructor and pass it a function with parameterresult - Inside the function, use your state setter to update the list and pass the
todoListfrom yourresultobject - View your application in the browser
You'll notice now that the list is being saved in localStorage but after refreshing the page it is reset to empty. This is because our other side-effect is overwriting the data before the asynchronous fetch is complete.
We need a way to know that the data is still loading before trying to update it in localStorage.
- After the
todoListstate declaration, create a new state variable namedisLoadingwith update function namedsetIsLoadingwith default valuetrue - Inside the second
useEffecthook (withtodoListdependency), add anifstatement to check thatisLoadingis false before settinglocalStorage
Now we just need to way to turn loading off once the data has been fetched.
- Revisit the
thencallback in the firstuseEffecthook - After setting the
todoListstate, add another line to setisLoadingstate tofalse - View your application in the browser
- Enter a new todo in "Add Todo" form and submit
- Reload the page and wait 2 seconds
- Notice that the saved item now appears in the list
Great! Now our data is being saved properly again, but that delay in loading makes it seem like the list is empty at first. Let's add a loading indicator to prevent confusion.
- Inside the
AppJSX, create a new paragraph element aboveTodoListwith text "Loading..." - View your application in the browser
- Reload the page and notice that the loading message is visible
- Wait 2 seconds and notice that the Todo List appears but the loading message is still there
We don't want to always show the loading indicator, it should conditionally appear based on our isLoading state.
- Using a ternary operator inside JSX, if
isLoadingistruerender the loading message, otherwise render theTodoListcomponent - View your application in the browser
- Reload the page and notice that the loading message is visible
- Wait 2 seconds and notice that the loading indicator disappears when the Todo List becomes visible