React API Loading States

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 loading state, starting at true, because we begin by waiting.
  • Run the fetch. When it finishes, whether it worked or not, set loading to false.
  • While loading is true, show “Loading…”. Once it’s false, 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.

  • loading starts as true, 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 then setLoading(false).
  • That state change re-renders the component, loading is now false, so it shows the list.

Output

(first the screen shows:)
Loading...
(a second later, it becomes:)
Leanne Graham
Ervin Howell
Clementine 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 try holds the normal path that fills the data.
  • The catch runs if the fetch fails, so you can handle the error.
  • The finally block 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 broken
const [loading, setLoading] = useState(false);
// ✅ starts true - first render shows "Loading..." right away
const [loading, setLoading] = useState(true);

Keep these in mind.

  • Don’t start loading at false. You begin by waiting, so start it true.
  • Don’t forget to set it false when done, or the loader never goes away.
  • Don’t set it false only on success. Use finally so a failure clears it too.

✅ Best Practices

A few habits for clean loading states.

  • Start loading as true, because the component begins by fetching.
  • Set it to false in a finally block so it always clears, success or failure.
  • Show the loading UI with a simple early return while loading is true.
  • 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 loading boolean in state, starting at true because you begin by waiting
  • ✅ Set loading to false once the fetch finishes, then show the data
  • ✅ Use an early return to show “Loading…” while loading is true
  • ✅ Set loading false in a finally block 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. 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. 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. 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. 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.

React API Error Handling

Share & Connect