Fetching Data
The data rendered in your React app may come from a variety of sources and in many cases, your components may need to “wait” for the data to be fetched before they can render.
In this chapter, we will go over common data sources and how to fetch and use that data in your React components.
You will learn
- Static and dynamic data and how it affects data access
- How to fetch data in your components and examples of common data sources
- How components “wait” for data to be fetched
Static and Dynamic Data
In our previous React examples, we’ve used local variables to hold data that our components use.
const ESTABLISHED_YEAR = 1986;
function CompanyFooter() {
return <p>Quality since {ESTABLISHED_YEAR}</p>;
}
In this component, the data is the year we store in the local variable ESTABLISHED_YEAR
.
Our app may also use ESTABLISHED_YEAR in other places so we can decouple the data from our UI and provide the data in a separate module.
import ESTABLISHED_YEAR from "./data";
function CompanyFooter() {
return <p>Quality since {ESTABLISHED_YEAR}</p>;
}
In both of these examples, the data is set and read once for CompanyFooter
. The examples read the data outside of the component function so every-time the component is rendered, it refers to the same data.
This works because the nature of the data in this example is unchanging. An establishment year for a company is a historical record that will not be updated in the future. We refer to this type of data as static.
On the other hand, there may be data that is ever-changing or unique to a user or location. Consider a Weather app that renders the temperature of a local city, the temperature data powering that app will frequently update. We refer to this type of data as dynamic data.
Generally, the labels “static” and “dynamic” refer to guarantees we can make about the data and who can access it. Static data usually means the data does not change often (yearly, monthly) or that its update schedule is predictable or controlled. As well, it can mean that the data is the same no matter who is asking for it.
Dynamic data may be updated by the minute or by users, which is unpredictable. It may also be unique to a certain user or locale-specific.
Illustrated by Rachel Lee Nabors
The reason why we distinguish static and dynamic data is it affects how often we need to fetch the data. For dynamic data, we want to display current, “fresh”, data to our users. To do this, we’ll need to re-fetch the data periodically in case of updates. One way to do this is to fetch data in a component function.
Fetching data in a component
Consider a temperature component in a weather app. In this example, we assume there is a local file that is continually updated with the current temperature in the city of Los Angeles.
import { readFile } from "node:fs/promises";
async function LATemperature() {
const temp = await readFile("/path/to/temperature");
return <p>The current temperature in Los Angeles is: {temp}</p>;
}
During render, we read the temperature and render it in the component. We use the function readFile
from the fs
package to read the local file. Don’t worry about the await
, async
keywords for now, we’ll dive deeper into that in the next section. The important thing is that we are reading a data source during the render function. Every time the component renders, the data is read again.
An important thing to note is that the component doesn’t know when the data has changed. The component must be told to re-render, and only then will it read “fresh” data.
Fetching data from a database
Beyond reading from a file, another common data source is a database. You can imagine a database like a collection of spreadsheets, with each individual spreadsheet being referred to as a “table”. Just like a spreadsheet, a table contains column names and every entry in the table is a row of data.
Imagine we have a table named ‘weather’ in a database, with the columns:
temp
- the temperature recordedcity
- the city the temperature was recordeddate
- the date of the recorded temperature
Here, we query our imaginary weather table in our component.
import { Client } from "pg";
import connectionConfig from "./config";
const client = new Client(connectionConfig);
await client.connect();
async function LATemperature() {
const temp = await client.query(
`SELECT temp, date FROM weather WHERE city = 'Los Angeles' ORDER BY date DESC`,
);
return (
<p>
The current temperature in LA is: {temp}. Last updated {date}.
</p>
);
}
First, we establish our database connection and then we send our query to the database to get the temperature.
Databases are often queried through a database server. Database servers may be running locally on your machine, or may be accessible through a network. connectionConfig
will contain the address of where the database server is running.
Fetching data from an API
Similar to database servers, there may be other services you can fetch data from. A common example is using a third-party service to access data you don’t own or maintain. These third-party services will provide an API, an interface for you to structure your data request.
Here is an example of a weather service that offers an API to get the latest temperature data for Los Angeles. To get the temperature, the API requires us provide the longitude and latitude of the location we’re interested in.
async function LATemperature() {
const { temperature } = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&hourly=temperature_2m`,
);
return <p>The current temperature in Los Angeles is: {temp}.</p>;
}
An API service will usually provide documentation on how to make a request to their server and how the data is returned to you.
Waiting for asynchronous data during render
You’ll notice that once we started to read data during render, we began to use the keywords async
and await
in our component. Let’s dive deeper into what that means.
import { readFile } from "node:fs/promises";
async function LATemperature() {
const tempPromise = readFile("/path/to/temperature");
const temp = await tempPromise;
return <p>The current temperature in LA is: {temp}</p>;
}
In our local file example, we imported the function readFile
from a file streaming package. readFile
is an asynchronous function that takes in a file path and returns the file contents. An asynchronous function means that JavaScript does not pause execution to wait for the function to finish. Instead, it returns a Promise, a container for its future return value.
An analogy to calling an asynchronous function is like taking your clothes to the dry cleaners. You’ve given the work to clean your clothes to someone else, leaving you free to do other things. The dry cleaners give you a ticket, a promise, that they will complete this work and notify you when they’re done. When the work is done successfully, the Promise is considered resolved.
Illustrated by Rachel Lee Nabors
Now, imagine you are trying to render the component LATemperature
. The first thing you do is kick off the asynchronous work of reading the temperature file and you’ve received tempPromise
as a Promise. Now, you’re free to do the next set of work. But wait, the next instruction requires you to know the temperature, yet all we have is a Promise. The Promise may or may not be ready with the actual temperature value.
Here is where await
comes in. await
is a keyword that is used before a Promise and tells JavaScript to pause running the current function and wait for the Promise to resolve.Then, when it is resolved, await will unwrap the Promise container to return the value. We use await on tempPromise to pause our component rendering until the file is read and receive the value returned.
In our analogy, this would be similar to waiting at the dry cleaners for them to finish because the next chore on your todo list requires those clothes. Thus, preventing you from doing anything else in the meantime.
Our other examples of fetching data from a database server or making a network request to an API endpoint are also asynchronous operations that return Promises.
Deep Dive
Generally, you’ll want to kick off asynchronous work as early as possible so that you can maximize the amount of work you can do before needing to await
the Promise returned.
Due to the simple nature of our component, our example needed to immediately await the asynchronous operation because the component render was blocked on that data.
Here is an example where we are able to kick off the asynchronous work and then do some synchronous work in the meantime, and then finally await the data just before it is used.
async function HoveringButton({ userContext, label, lastTouchPoint }) {
// async operation
const localePromise = getLocale(userContext);
// expensive calculation
const offset = calculateNewOffset(lastTouchPoint);
return ...
}
You’ll notice that our component function declarations now have an async
prefix. This is also a JavaScript keyword and any function that uses await
will need to be marked with async
. If a function await
s a Promise, it itself becomes asynchronous.
Imagine being the parent function calling the LATemperature
component function. When you call LATemperature
and it gets to the instruction of await
-ing readFilePromise
, JavaScript execution returns to you and you receive a Promise that promises to give you the completed render of LATemperature
.
Deep Dive
TODO: go into build-time and run-time data access
Recap
- Data is the content we display in our React apps.
- Static data refers to data that changes infrequently or within your control.
- Dynamic data refers to data that changes frequently or unpredictably.
- For dynamic data, you’ll likely want to read your data in your component function so that it reads “fresh” data for every render.
- Accessing during component render, is often an asynchronous operation.
- Asynchronous functions perform work asynchronously to its calling function. They return Promises, a container for its future return value.
- You can tell your React components to wait for an asynchronous operation by await-ing the Promise returned.