π Fetching data
Learning Objectives
So far we have displayed film data stored in our JavaScript code. But real applications fetch data from servers over the internet. We can restate our problem as follows:
Given an API that serves film data
When the page first loads
Then the page shouldfetch
and display the list of film data, including the film title, times, and film certificate.
π» Client side and π Server side APIs
We will use fetch()
, a
APIs are useful because they let us get information which we don’t ourselves know. The information may change over time, and we don’t need to update our application. When we ask for the information, the API will tell us the latest version.
We also don’t need to know how the API works in order to use it. It may be written in a different programming language. It may talk to other APIs we don’t know about. All we need to know is how to talk to it. This is called the interface.
Using fetch is simple. But we want to understand what is happening more completely. So let’s take ourselves on a journey through time.
ππΎ Unfurl to see the journey (we will explain this in little pieces)
π΅βπ« This is a lot to take in. Let’s break it down and make sense of it.
Youtube: Step-through-prep workshop
ποΈ Latency
Learning Objectives
Instead of already having our data, we are now sending a request over the network to another computer, and then waiting for that computer to send us a response back. Now that our data is going on a journey over a network, we introduce the problem of latency.
Latency is the time taken for a request to traverse the network.
π‘ Network latency is travel time.
Why is latency a problem? Because it means we need to wait for our data. But our program can only do one thing at a time. If we stopped our program to wait for data, then we wouldn’t be able to do anything else (like show the rest of the page, or respond to a user clicking in the page). We need to handle this time problem.
Programming often involves time problems, and latency is just one of them.
β³ Synchronous execution
Learning Objectives
We can handle latency using
We have written a lot of JavaScript programs that execute sequentially. This means that each line of code is run in order, one after the other.
For example:
console.log("first");
console.log("second");
console.log("third");
Outputs:
first
second
third
When we call a function, the function will run to completion before the next line of code is executed. But what if we need to wait for something to happen? What if we need to wait for our data to arrive before we can show it? In this case, we can use asynchronous execution.
πͺ Callbacks
Learning Objectives
Consider this visualisation of an asynchronous program:
ππ½ Code running out of order and off the thread
When we call setTimeout
we send a function call to a client side Web API. The code isn’t executing in our single thread any more, so we can run the next line. The countdown is happening, but it’s not happening in code we control.
When the time runs out, the Web API sends a message to our program to let us know. This is called an
π‘Our call is back
With a pen and paper, draw a diagram of your mental model of the event loop.
Use your model to predict the order of logged numbers in the following code snippet:
setTimeout(function timeout1() {
console.log("1");
}, 2000);
setTimeout(function timeout2() {
console.log("2");
}, 500);
setTimeout(function timeout3() {
console.log("3");
}, 0);
console.log("4");
Did yours look different? There are many ways to visualise the event loop. Work on building your own mental model that helps you predict how code will run.
π Requesting from a server side API
Learning Objectives
So now we have these pieces of our giant concept map:
- π€ We know that we can send a request using
fetch()
- π We know that
fetch
is a π» client-side π§° Web API that requires an HTTP connection - ποΈ We know that sending requests over a network takes time
- 𧡠We know that we should not stop our program to wait for data
- πͺ We know that we can use Promises to manage asynchronous operations
But we still donβt know how to use fetch
to get data from a server side API.
Loading html files
When you double-click an HTML file in your file explorer to open it directly in your browser, you’re using what’s called the “file protocol” or “file scheme.” In your browser’s URL bar, you’ll see something like:
file:///Users/username/projects/my-website/index.html
The file://
prefix indicates that your browser is reading the file directly from your computer’s filesystem, without going through a web server. While this approach might seem convenient for simple HTML files, it will prevent us from using fetch
.
Web Server Access: The HTTP Protocol
Another approach involves using a local development server. You can create one using tools like Python’s built-in server or npm’s http-server. These tools create a web server on your computer that serves your files using the HTTP protocol. Your browser will then access the files through a URL like:
http://localhost:8000/index.html
The http://
prefix shows that you’re accessing the file through a proper web server, even though that server is running on your own computer.
You need to be using http://
(or https://
) not file://
in order to use fetch
.
Using fetch
Previously, we had a list of films hard-coded in our state
. Now, let’s continue using our concept map to fetch data from a server.
// Begin with an empty state
const state = {
films: [],
searchTerm: "",
};
const endpoint = "https://programming.codeyourfuture.io/dummy-apis/films.json";
const fetchFilms = async () => {
const response = await fetch(endpoint);
return await response.json();
};
fetchFilms().then((films) => {
state.films = films;
render();
});
Serving files locally
file://
URLs, that’s your signal to serve your files through a local development server instead of opening them directly in the browser.fetch returns a Promise; the Promise fulfils itself with a response; the response contains our data.
Next, we’ll dig into Promise
s, async
, await
, and then
in more detail to complete our concept map.
π«±πΏβπ«²π½ Promises
Learning Objectives
To get data from a server, we make a request with fetch
. We act on what comes back: the response. But what happens in the middle? We already know that JavaScript is single-threaded: it can only do one thing at a time.
So do we just stop and wait? No! We have a special object to handle this time problem. Put this code in a file and run it with node:
const url = "https://api.github.com/users/SallyMcGrath"; // try your own username
const response = fetch(url);
console.log(response);
Your Promise should look like this:
Promise {
Response {
[Symbol(realm)]: null,
[Symbol(state)]: {
aborted: false,
rangeRequested: false,
timingAllowPassed: true,
requestIncludesCredentials: true,
type: 'default',
status: 200,
timingInfo: [Object],
cacheState: '',
statusText: 'OK',
headersList: [HeadersList],
urlList: [Array],
body: [Object]
},
[Symbol(headers)]: HeadersList {
cookies: null,
[Symbol(headers map)]: [Map],
[Symbol(headers map sorted)]: null
}
},
[Symbol(async_id_symbol)]: 54,
[Symbol(trigger_async_id_symbol)]: 30
}
The response
variable in this code is not labelling the data. It’s labelling a Promise
.
A promise is exactly what it sounds like: a promise to do something. You can use this promise object to sequence your code. You can say, “When the data comes back, then
do this.”
You will explore Promises in more detail as you build more complex applications. For now, let’s move on to .then()
.
πͺ .then()
Learning Objectives
.then()
is a method that all Promise
s have. You can interpret this code:
const url = "https://api.github.com/users/SallyMcGrath";
const callback = (response) => response.json(); // .json() is an instance method that exists for all Response objects.
fetch(url).then(callback);
- given a request to
fetch
some data - when the
response
comes back / the promise resolves to a response object then
do this next thing with the data / execute this callback
The .then()
method takes in a callback function that will run once the promise resolves.
We can also inline the callback
variable here - this code does exactly the same as the code above:
const url = "https://api.github.com/users/SallyMcGrath";
fetch(url).then((response) => response.json());
It’s a similar idea as the event loop we have already investigated, but this time we can control it clearly. The .then()
method queues up callback functions to execute in sequence once the asynchronous operation completes successfully. This allows us to write code as if it was happening in time order.
π‘tip
then()
method of a Promise
always returns a new Promise
.We can chain multiple .then()
calls to run more logic, passing the resolved value to the next callback in the chain. This allows us to handle the asynchronous response in distinct steps. Let’s create a getProfile function in a file, call it, and try running the file with node:
const getProfile = (url) => {
return fetch(url)
.then((response) => response.json()) // This callback consumes the response string and parses it as JSON into an object.
.then((data) => data.html_url) // This callback takes the object and gets one property of it.
.then((htmlUrl) => console.log(htmlUrl)); // This callback logs that property.
};
getProfile("https://api.github.com/users/SallyMcGrath");
So then
returns a new Promise
, and you can call then
again on the new object. You can chain Promises in ever more complex dependent steps. This is called Promise chaining.
It’s important to understand some of what is happening with Promises and then
. But for the most part, you will not be writing code in this style.
π¬ async/await
Learning Objectives
These two blocks of code do exactly the same thing:
const getProfile = async (url) => {
const response = await fetch(url);
const data = await response.json();
const htmlUrl = data.html_url;
console.log(htmlUrl);
};
getProfile("https://api.github.com/users/SallyMcGrath");
const getProfile = (url) => {
return fetch(url)
.then((response) => response.json())
.then((data) => data.html_url)
.then((htmlUrl) => console.log(htmlUrl));
};
getProfile("https://api.github.com/users/SallyMcGrath");
Async/await is
We group async
and await
together: async/await, because we await
inside an async
function or at the top level of a module.
We use the async
keyword to define a function that returns a Promise. An async function always returns a Promise.
We can see this with a simple function which doesn’t need to await anything. Save this in a file and run it with node:
const getProfile = async (url) => url;
console.log(getProfile("hello")); // Logs a Promise.
getProfile("hello").then((value) => console.log(value)); // Logs a value
Even though the function above doesn’t have a time problem, the fact that we define the function as an async
function means it returns a Promise
.
But let’s do something more interesting - let’s actually solve a time problem.
const getProfile = async (url) => {
// the async keyword tells us this function handles a time problem
};
We use the await
operator to say “don’t move on until this is done”. Importantly, we are not actually waiting for a Promise to resolve. We are scheduling a callback that will be called when the Promise resolves. But this allows us to write code that looks like it’s happening in time order (as if we are waiting), without actually blocking our main thread.
const getProfile = async (url) => {
const response = await fetch(url);
return response.json();
};
getProfile("https://api.github.com/users/SallyMcGrath").then((response) =>
console.log(response)
);
Save this to a file and run with with node. It works the same as before.
π ποΈ Fetch Films
Learning Objectives
Now that we have a basic understanding of Web APIs and Promises, let’s use look again at our code for fetching film data:
const endpoint = "https://programming.codeyourfuture.io/dummy-apis/films.json";
const fetchFilms = async () => {
const response = await fetch(endpoint);
return await response.json();
};
fetchFilms().then((films) => {
// When the fetchFilms Promise resolves, this callback will be called.
state.films = films;
render();
});
We are defining fetchFilms
: an async
function - a function which returns a Promise
.
When we call fetchFilms
, what we get is an unresolved Promise
.
What fetchFilms
does is fetch a URL (with our call to fetch
itself returning a Promise
resolving to a Response
). When the Promise
from fetch
resolves, fetchFilms
reads the body of the Response
(a string), and parses is as JSON. The Promise
returned by fetchFilms
then resolves with the result of parsing the string as JSON.
When the Promise
from fetchFilms
resolves, our next callback is called: We update our state
, and call render()
.
After this is done, the rest of our code works exactly the same as it did before. We have our list of films in our state, so we never need to fetch the list of films again.
render
works the same - it only cares that state.films
is an array of films, it doesn’t care where they came from.
When we change our filter by typing, events fire and our event handler will be called back exactly the same as it did before.