React & fetch
Learn how to fetch data from APIs in your React components
- js
- react
- fetch
- effects
React doesn’t have a built-in pattern designed for fetching data. This can make it a little confusing at first, so let’s look at how we can combine useState
and useEffect
to create a GitHub profile page.
Setup
- Download starter files and
cd
in npm install
npm run dev
The index.html
file loads workshop/index.jsx
. This imports workshop/App.jsx
and renders the component using React.
Managing effects
React components are designed to keep the DOM in-sync with your app’s data. For example this component will re-render every time the name
prop changes, ensuring the message is always correct:
function Greeting({ name }) {
return <div>Hello {name}!</div>;
}
However some parts of your app cannot be represented with JSX, as they are not part of the DOM. React calls these “effects”—they are extra things your component does (other than the primary task of rendering DOM elements).
In order to ensure React can keep track of these effects and re-run them when our app’s data changes we pass them in to the React.useEffect
function.
Fetching data
Fetching data is one of these “effects”. We run our fetch
request inside useEffect
so React can control when it runs (or re-runs).
function Pokemon() {
React.useEffect(() => {
fetch("https://pokeapi.co/api/v2/pokemon/pikachu")
.then((res) => res.json())
.then((data) => console.log(data));
});
return <div>Hello</div>;
}
We have a problem here: our API request could take 10 seconds to finish. However React components are synchronous—they must render something straight away. We cannot wait for the response to be done before returning a value.
Instead we need to update our component with the new data once the response finishes. We can make a component update by setting state. Remember that a component will re-run whenever its state values change.
function Pokemon() {
const [pokeData, setPokeData] = React.useState(null);
React.useEffect(() => {
fetch("https://pokeapi.co/api/v2/pokemon/pikachu")
.then((res) => res.json())
.then((data) => setPokeData(data));
});
return <div>Hello</div>;
}
We can’t use this data immediately, since the API request is asynchronous. Our component will render at least once with the initial state, which here is null
.
The easiest way to make sure the data has loaded before we use it is to check whether the state variable is there:
function Pokemon() {
const [pokeData, setPokeData] = React.useState(null);
React.useEffect(() => {
fetch("https://pokeapi.co/api/v2/pokemon/pikachu")
.then((res) => res.json())
.then((data) => setPokeData(data));
});
if (!pokeData) {
return <div>Loading...</div>;
} else {
return <div>{pokeData.name}</div>;
}
}
Here’s the flow of our component’s updates:
- The component is rendered (i.e.
<Pokemon />
somewhere) - React calls the
Pokemon
function - React creates the
pokeData
state (because we calleduseState
) - React queues an effect to run (because we called
useEffect
) pokeData
isnull
so JS runs the firstif
branch- The component returns
<div>Loading...</div>
- The queued effect runs, which sends a
fetch
request
Some time passes…
- The
fetch
request resolves with the response data - Our
.then
sets thepokeData
state as the response object - React sees the state update and re-runs the component function
- This time the
pokeData
state variable is the response object (notnull
) - So JS runs the second
if
branch and returns<div>pikachu</div>
Avoiding infinite loops
There is one final problem to solve: our component currently always queues a new effect. This means that after our component’s state updates (and re-renders the component) it’ll send a new fetch
request. When this request resolves it’ll update the state, re-rendering the component. This will trigger another fetch
, and so on.
To avoid this infinite loop we need to constrain when the effect runs, by providing the dependencies array as the second argument. This tells React that the effect only needs to re-run if the things inside the array have changed.
In this case our effect has no dependencies, since it doesn’t use any values from outside the effect. So we can specify an empty array:
React.useEffect(() => {
// ...
}, []);
This tells React “you won’t need to re-run this effect, since it doesn’t depend on any values that might change and get out of sync”.
It’s really important not to forget this dependency array: if you trigger an infinite fetching loop GitHub might temporarily ban you from their API!
Challenge 1: user profile
You’re going to build a Profile
component that fetches a user from the GitHub API and renders their name, avatar image and any other details you like.
- Create a new component in
workshop/Profile.jsx
- It should fetch your profile from
"https://api.github.com/users/{username}"
- It should render a loading message until the request is done
- It should render at least your name & avatar image once the request completes
Don’t forget to import and render this component in workshop/App.jsx
.
Toggle answer
// Profile.jsx
import React from "react";
const USER_URL = "https://api.github.com/users/";
function Profile() {
const [user, setUser] = React.useState();
React.useEffect(() => {
fetch(USER_URL + "oliverjam")
.then((res) => res.json())
.then((data) => setUser(data));
}, []);
if (!user) return <div>Loading...</div>;
return (
<div>
<h1>{user.name}</h1>
<img src={user.avatar_url} alt="" width="128" height="128" />
</div>
);
}
export default Profile;
Re-running effects
Our Profile
component would be more useful and reusable if it could fetch any user’s GitHub information. Components can be customised by passing in props (just like function arguments). We want to be able to do this:
<Profile name="oliverjam" />
and have the component fetch that user’s profile.
Challenge 2: re-usable profile
- Amend
Profile
to take aname
prop - Use this prop to fetch the right data from GitHub
- Pass a
name
to<Profile />
insideApp
Hint: you’ll need to tell useEffect
about this new dependency, since it will need to re-run your fetch if/when the name
prop changes.
Toggle answer
const USER_URL = "https://api.github.com/users/";
function Profile(props) {
const [user, setUser] = React.useState();
React.useEffect(() => {
fetch(USER_URL + props.name)
.then((res) => res.json())
.then((data) => setUser(data));
}, [props.name]);
if (!user) return <div>Loading...</div>;
return (...);
}
export default Profile;
Note that we had to pass props.name
to useEffect
inside the dependency array. Otherwise this would only ever fetch the first name passed in. Later we’ll add a feature that lets the user type in a name, so we will need the effect to re-run.
Passing state down
Our Profile
component can now fetch any user, but we still have to hard-code the prop when we render it in App
. Ideally we’d let users type the name into a search input, then update the prop we pass down when they submit.
We can achieve this with a state value in App
that keeps track of the current value of name
. When the form is submitted you can update that state value, which will cause the App
component to re-render. This will then cause Profile
to re-render with the new value of name
passed as a prop.
Challenge 3: searching for users
- Add a form with a search input to
App
- Add a
name
state value toApp
- When the form is submitted update the state value
- Pass the state value to
Profile
so it knows which name to fetch
Hint: Don’t forget to call event.preventDefault()
in your submit handler to stop the form from refreshing the page.
Toggle answer
function App() {
const [name, setName] = React.useState("oliverjam");
return (
<main>
<form
onSubmit={(event) => {
event.preventDefault();
setName(event.target.username.value);
}}
>
<input
type="search"
aria-label="Search users"
placeholder="Search users"
name="username"
/>
</form>
<Profile name={name} />
</main>
);
}
Note that we did not need to change Profile
at all. This is because we have divided up each component’s responsibilities. Profile
just takes a username and fetches that data—it doesn’t care how it gets the name.
Stretch goal: fetching repos
The user response object from GitHub contains a repos_url
property. This is a URL from which you can fetch an array of the user’s repositories. To display the user’s repos underneath their other info we’ll have to make another fetch after the first one resolves.
The simplest way to achieve this is by creating a new component that takes the repos_url
as a prop, fetches the data, then renders the list of repos.
- Create a new component in
ReposList.jsx
- It should receive a URL as a prop and fetch the repos from it
- When it receives a response it should render a list of repos
- Amend
Profile
to renderReposList
and pass in the right URL prop
Toggle answer
// Profile.jsx
function Profile({ name }) {
// ...
return (
<div>
<h1>{user.name}</h1>
<img src={user.avatar_url} alt="" width="128" height="128" />
<h2>Repos</h2>
<ReposList url={user.repos_url} />
</div>
);
}
// ReposList.jsx
import React from "react";
function ReposList({ url }) {
const [repos, setRepos] = React.useState();
React.useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => setRepos(data));
}, [url]);
if (!repos) return <div>Loading repos...</div>;
return (
<ul>
{repos.map((repo) => (
<li key={repo.id}>
<a href={repo.url}>{repo.name}</a> | ⭐️ {repo.stargazers_count}
</li>
))}
</ul>
);
}
export default ReposList;