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.
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:
JSXimport { 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:
- Add a
const [isLoading, setIsLoading] = useState(true);line to your component. - Wrap your
fetchcall logic to setisLoading(true)at the start andsetIsLoading(false)inside afinallyblock. - 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
isLoadingistrue. - 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
finallyblock: As mentioned, if you only setisLoading(false)inside the.then()or after theawait, 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.
Work with me

Headless WordPress + Next.js Frontend Development
Keep WordPress for content, get a lightning-fast Next.js frontend. The best of both worlds — familiar editing, modern speed.

React & Next.js Dashboard / Admin UI Development
A clean, data-rich dashboard UI in React or Next.js — charts, tables, and real-time data that your users will actually enjoy using.