React API Loading States
Table of Contents + −
In the last lesson, React Async and Await, you wrote clean fetch code with async/await. This lesson adds a loading state so the screen shows “Loading…” while the request runs, then swaps to the data the moment it arrives.
🤔 Why do we need a loading state?
Here’s the pain. A fetch isn’t instant, and for that gap between asking and getting the answer, the user sees nothing.
- The page looks empty or frozen, so the user wonders if it’s broken.
- They might click again, or leave, thinking nothing is happening.
- On a slow connection that gap can be several seconds, which feels much worse.
- Good apps always tell the user “I’m working on it”, so the wait feels normal.
So a loading state is just honest feedback. It tells the user the data is on its way.
🧩 The idea: track loading in state
The trick is to keep a boolean in state that says whether we’re still waiting.
- Add a
loadingstate, starting attrue, because we begin by waiting. - Run the fetch. When it finishes, whether it worked or not, set
loadingtofalse. - While
loadingistrue, show “Loading…”. Once it’sfalse, show the data.
So loading is a simple on/off switch. On means “still fetching”, off means “done, show the result”.
💡 A loading state in action
Let’s add it to a user list. We track both the users and a loading flag, and show the message until the data lands.
import { useState, useEffect } from "react";
function UserList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); // start true: we begin waiting
useEffect(() => { async function loadUsers() { const response = await fetch("https://jsonplaceholder.typicode.com/users"); const data = await response.json(); setUsers(data); setLoading(false); // data is in, stop loading } loadUsers(); }, []);
// while waiting, show the loading message if (loading) { return <p>Loading...</p>; }
// once done, show the data return ( <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> );}Walk through the whole sequence.
loadingstarts astrue, so the very first render returns “Loading…”.- After that render, the effect runs and fetches the users.
- When the data arrives, we call
setUsers(data)and thensetLoading(false). - That state change re-renders the component,
loadingis nowfalse, so it shows the list.
Output
(first the screen shows:)Loading...
(a second later, it becomes:)Leanne GrahamErvin HowellClementine Bauch...🛡️ Turn off loading even when it fails
Here’s a subtle bug to avoid. If the fetch fails and you only set loading = false on success, the screen is stuck on “Loading…” forever. So turn it off no matter what.
async function loadUsers() { try { const response = await fetch("https://jsonplaceholder.typicode.com/users"); const data = await response.json(); setUsers(data); } catch (error) { console.log("Failed:", error); } finally { setLoading(false); // ✅ runs whether it worked or failed }}Notice the finally block, because that’s the key.
- The
tryholds the normal path that fills the data. - The
catchruns if the fetch fails, so you can handle the error. - The
finallyblock runs in both cases, success or failure. - Putting
setLoading(false)there means loading always stops, so the screen never gets stuck.
Always stop loading
If you only set loading to false after a successful fetch, a failed request leaves the user staring at “Loading…” forever. Use a finally block, or set it false in both the success and error paths.
🎨 Make the loading message nicer
“Loading…” works, but you can show anything while waiting. The pattern is the same, you just return different UI.
if (loading) { return ( <div className="loader"> <p>Loading users, please wait...</p> </div> );}A few easy upgrades you can make later.
- Show a spinner, a small animated circle, instead of plain text.
- Show a skeleton, gray boxes shaped like the content that’s coming.
- Keep the layout steady so the page doesn’t jump when the real data replaces the loader.
So the loading branch is just normal JSX. Start with simple text, and make it fancier whenever you like.
⚠️ Common Mistakes
The most common mistake is starting loading as false, so the user sees an empty screen before loading even begins.
// ❌ starts false - first render shows nothing, looks brokenconst [loading, setLoading] = useState(false);
// ✅ starts true - first render shows "Loading..." right awayconst [loading, setLoading] = useState(true);Keep these in mind.
- Don’t start
loadingatfalse. You begin by waiting, so start ittrue. - Don’t forget to set it
falsewhen done, or the loader never goes away. - Don’t set it
falseonly on success. Usefinallyso a failure clears it too.
✅ Best Practices
A few habits for clean loading states.
- Start
loadingastrue, because the component begins by fetching. - Set it to
falsein afinallyblock so it always clears, success or failure. - Show the loading UI with a simple early
returnwhileloadingistrue. - Keep the loading UI close in size to the real content, so the page doesn’t jump.
Three states are coming
Right now you handle loading and success. There’s still a third case: the request failed. Next you’ll add an error state too, so your component cleanly handles all three: loading, success, and error.
🧩 What You’ve Learned
- ✅ A loading state tells the user the request is working, instead of showing a blank screen
- ✅ Keep a
loadingboolean in state, starting attruebecause you begin by waiting - ✅ Set
loadingtofalseonce the fetch finishes, then show the data - ✅ Use an early
returnto show “Loading…” whileloadingistrue - ✅ Set
loadingfalse in afinallyblock so it clears on both success and failure - ✅ The loading branch is just JSX, so it can be plain text, a spinner, or a skeleton
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
What value should the loading state start at?
Why: You begin by fetching, so you are waiting from the very first render. Starting loading at true shows the loading message right away.
- 2
Why set loading to false inside a finally block?
Why: finally runs whether the try succeeds or the catch handles an error. Putting setLoading(false) there guarantees the loader always goes away.
- 3
What happens if you only set loading to false after a successful fetch?
Why: If the fetch fails and you never set loading to false, the loading branch keeps showing and the user is stuck. Clear it in both paths or in finally.
- 4
How do you show the loading message in the component?
Why: A common pattern is an early return: if loading is true, return the loading UI; otherwise fall through and return the data.
🚀 What’s Next?
Now the user sees “Loading…” while waiting, then the real data. But what if the request fails? Right now that case is ignored. Next you’ll add proper error handling so a failed request shows a friendly message instead of a broken page.