Back to Blog
Lesson 26 of the React Fundamentals: Build Modern UIs from Scratch course
ReactJune 25, 20263 min read

Handling Loading States in React: Improving UX and Performance

Learn to master loading state in React to improve UX. Discover how to conditionally render spinners while your API fetches data for your movie app.

Reactloading stateUXconditional renderinghooksstate managementjavascriptfrontend

Previously in this course, we covered fetching data from an API to populate our application. While that allows us to retrieve data, it introduces a "gap" in the user experience: the time between the request being sent and the data arriving. If we simply leave the UI blank during this interval, users often assume the app is broken.

In this lesson, we will implement a robust loading state to bridge that gap. By managing a dedicated state variable and applying conditional rendering, we can show the user that something is happening, significantly improving the perceived performance of our movie browser.

Why Loading States Matter for UX

In a real-world application, network latency is unpredictable. Even a fast API can take 500ms to respond, which feels like an eternity on the web. By explicitly showing a "Loading..." message or a CSS spinner, we provide immediate feedback. This simple shift in UX transforms a perceived "hang" into a managed data-fetching process.

Managing Loading State

To track whether our data is currently being fetched, we introduce a new piece of state. We’ll use a boolean value: true when the request is in progress, and false once the data is returned or an error occurs.

Here is how we integrate this into our existing MovieList component:

JSX
import { useState, useEffect } from CE9178">'react';

function MovieList() {
  const [movies, setMovies] = useState([]);
  const [isLoading, setIsLoading] = useState(true); // 1. Initialize as true

  useEffect(() => {
    async function fetchMovies() {
      setIsLoading(true); // 2. Set to true before the fetch
      try {
        const response = await fetch(CE9178">'https://api.example.com/movies');
        const data = await response.json();
        setMovies(data);
      } catch (error) {
        console.error("Failed to fetch", error);
      } finally {
        setIsLoading(false); // 3. Always set to false when done
      }
    }

    fetchMovies();
  }, []);

  // 4. Conditional Rendering based on state
  if (isLoading) {
    return <div className="spinner">Loading movies...</div>;
  }

  return (
    <ul>
      {movies.map(movie => <li key={movie.id}>{movie.title}</li>)}
    </ul>
  );
}

The "Finally" Pattern

Notice the use of the finally block in the example above. This is a best practice. Whether the API call succeeds or fails, the loading state must be set back to false. Without it, if the API throws an error, your user would be stuck with a permanent "Loading..." indicator—a classic "dead-end" UI bug.

Hands-on Exercise: Add a Spinner to Your App

Building on our movie-browser project, locate your movie fetching component. Perform the following steps:

  1. Add a const [isLoading, setIsLoading] = useState(true); line to your component.
  2. Wrap your fetch call logic to set isLoading(true) at the start and setIsLoading(false) inside a finally block.
  3. In your JSX, use a ternary operator or an early return statement (as shown above) to display a simple paragraph or a CSS-styled spinner component when isLoading is true.
  4. Test this by opening your browser's "Network" tab in DevTools, setting the throttling to "Slow 3G," and refreshing the page to see your loading indicator in action.

Common Pitfalls

  • Forgetting the finally block: As mentioned, if you only set isLoading(false) inside the .then() or after the await, an error will leave your application in a permanent loading state.
  • Over-complicating state: Beginners often try to track "loading," "success," and "error" as separate state variables that might conflict. Start with a simple boolean. You can always evolve this into a state machine if your requirements grow.
  • Flickering indicators: If your API is extremely fast, the loading state might only appear for a few milliseconds, causing a "flicker." In production, some developers add a small delay to ensure the spinner is visible for at least 300ms to avoid visual noise.

Recap

Managing a loading state is a fundamental step in building professional React applications. By tracking the lifecycle of an asynchronous request, you can provide clear visual feedback, which is essential for a high-quality UX. We’ve learned to initialize our state, use finally to ensure clean transitions, and use conditional rendering to swap our view based on the current operation status.

Up next: Now that we have a loading state, we need to handle scenarios where the API request fails entirely; we'll cover managing errors in our next lesson.

Similar Posts