Loading and Error States
In this page:
The Three States of a Data Request
Any data-fetching UI generally needs to represent three distinct situations: still waiting for the response (loading), the request failed (error), and the response arrived successfully (success, with actual data to show). Designing for all three upfront avoids a UI that looks broken during real-world network conditions.
Note: Sketch out what each of the three states should look like before writing any fetching code — it clarifies what state variables you actually need.
Warning: Only designing for the success case means users see a blank or frozen screen during loading, or nothing at all when a request fails.
Example: The Three States of a Data Request
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
const [status] = React.useState("loading");
if (status === "loading") return <p>Loading...</p>;
if (status === "error") return <p>Something went wrong.</p>;
return <p>Data loaded successfully!</p>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Using One Status String Instead of Multiple Booleans
Tracking isLoading and isError as two separate booleans allows nonsensical combinations, like both being true at once. A single status field with distinct string values (idle, loading, error, success) can only ever represent one state at a time, which is a more accurate model of reality.
Note: This pattern is sometimes called a 'state machine' — only one named state is ever active.
Warning: Two independent booleans can drift out of sync over time as more code paths are added, silently creating impossible combinations.
Example: Using One Status String Instead of Multiple Booleans
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
const [status, setStatus] = React.useState("idle");
const startLoading = () => setStatus("loading");
return (
<div>
<button onClick={startLoading}>Load Data</button>
<p>Status: {status}</p>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Giving the Error State a Retry Option
A good error state doesn't just say 'something went wrong' — it offers a way to try again, like a Retry button that re-runs the original fetch logic and resets the status back to loading.
Note: Extract the fetching logic into its own function so both the initial load and the Retry button can call the exact same code.
Warning: An error message with no way to recover (besides a full page refresh) is a frustrating dead end for users.
Example: Giving the Error State a Retry Option
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
const [status, setStatus] = React.useState("error");
const retry = () => setStatus("loading");
if (status === "error") return <div><p>Failed to load.</p><button onClick={retry}>Retry</button></div>;
return <p>Status: {status}</p>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
- Only handling the success case and forgetting to design what the UI looks like while loading or after an error.
- Using a single boolean for three possible states, making 'loading AND error at once' representable when it shouldn't be.
- Not resetting the error state when a new request starts, leaving a stale error visible during a retry.
- A data-fetching component typically has three states: loading, error, and success (with data).
- Using a single status string (idle/loading/error/success) avoids invalid state combinations that separate booleans allow.
- Each state should have its own distinct UI: a spinner/message, an error message with retry option, and the actual content.
- Resetting error/loading state at the start of a new request keeps the UI accurate during retries.
No React-version restriction — built from useState and conditional rendering.
Chapter Quiz — Complete all 10 topics to unlock
0/10 topics done
Complete these topics first: