When a React application communicates with an API, the request may take some time to complete or may fail. A good application should clearly handle loading, success, and error states.
A loading state tells the application that some asynchronous operation is currently in progress.
For example, while student data is being fetched from an API, you can show:
Loading students...
This prevents the user from thinking that the application is not responding.
The useState Hook can be used to store the loading status.
const [loading, setLoading] = useState(true);
Here, true means that the operation is initially considered to
be loading.
You can conditionally display a loading message.
if (loading) {
return <p>Loading...</p>;
}
When loading is true, the component displays the loading message.
After the asynchronous operation finishes, the loading state can be changed
to false.
setLoading(false);
This allows the application to display the actual content.
An error state stores information when an operation fails.
For example, an API request can fail because of:
You can create an error state with useState.
const [error, setError] = useState("");
An empty string means that there is currently no error message.
The catch() method can be used to handle a rejected Promise.
fetch("/api/students")
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.log(error);
});
The error message can be stored in React state.
.catch(error => {
setError(error.message);
});
The component can then use the error state to display an appropriate message.
With fetch(), HTTP error responses do not automatically reject
the Promise. You can check response.ok.
fetch("/api/students")
.then(response => {
if (!response.ok) {
throw new Error(
"Request failed"
);
}
return response.json();
});
You can conditionally display an error message.
if (error) {
return (
<p>
Error: {error}
</p>
);
}
This gives the user information when something goes wrong.
The same loading state pattern can be used with Axios.
const [loading, setLoading] = useState(true);
axios.get("/api/students")
.then(response => {
setStudents(response.data);
})
.finally(() => {
setLoading(false);
});
Axios requests can handle errors using catch().
axios.get("/api/students")
.then(response => {
setStudents(response.data);
})
.catch(error => {
setError(error.message);
});
The finally() method runs after a Promise settles, whether it
was fulfilled or rejected.
axios.get("/api/students")
.then(response => {
setStudents(response.data);
})
.catch(error => {
setError(error.message);
})
.finally(() => {
setLoading(false);
});
An API-based component commonly has three important states:
Loading
↓
Request
↓
Success
or
Loading
↓
Request
↓
Error
React conditional rendering can display different content based on the current state.
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Error: {error}</p>;
}
return <StudentList />;
A component may start with an empty data array.
const [students, setStudents] = useState([]);
const [loading, setLoading] = useState(true);
The loading message can be shown until the API request completes.
An API request can succeed but return an empty array. This is different from an error.
if (students.length === 0) {
return <p>No students found.</p>;
}
The application can provide a useful message instead of displaying a blank screen.
A retry button can call the same API request again after a failure.
function retryRequest() {
setError("");
loadStudents();
}
Retry functionality can be useful when a temporary network problem occurs.
The try...catch statement can handle errors when using
async/await.
async function loadStudents() {
try {
const response = await fetch(
"/api/students"
);
const data = await response.json();
setStudents(data);
} catch (error) {
setError(error.message);
}
}
The finally block can be used to stop the loading state after
the request finishes.
async function loadStudents() {
try {
const response = await fetch(
"/api/students"
);
if (!response.ok) {
throw new Error(
"Failed to load students"
);
}
const data = await response.json();
setStudents(data);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
}
Instead of plain text, you can display a Bootstrap spinner while data is loading.
{loading && (
<div className="spinner-border">
</div>
)}
A spinner provides a visual indication that the application is working.
Bootstrap alert classes can be used to display errors.
{error && (
<div className="alert alert-danger">
{error}
</div>
)}
This creates a clear visual error message.
const [students, setStudents] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
fetch("/api/students")
.then(response => {
if (!response.ok) {
throw new Error(
"Unable to load students"
);
}
return response.json();
})
.then(data => {
setStudents(data);
})
.catch(error => {
setError(error.message);
})
.finally(() => {
setLoading(false);
});
}, []);
if (loading) {
return (
<div className="text-center">
<div
className="spinner-border"
>
</div>
<p>
Loading students...
</p>
</div>
);
}
if (error) {
return (
<div className="alert alert-danger">
<h4>
Something went wrong
</h4>
<p>
{error}
</p>
</div>
);
}
The dependency array of useEffect should be considered
carefully when making API requests.
useEffect(() => {
loadStudents();
}, []);
For an effect that should run once after the initial render, an empty dependency array is commonly used.
Start
↓
loading = true
↓
API Request
↓
┌───────────────┐
│ │
Success Error
│ │
↓ ↓
Data Error
│ │
└───────┬───────┘
↓
loading = false
↓
Display UI
import {
useEffect,
useState
} from "react";
function Students() {
const [students, setStudents] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
async function loadStudents() {
try {
const response = await fetch(
"/api/students"
);
if (!response.ok) {
throw new Error(
"Unable to load students"
);
}
const data = await response.json();
setStudents(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
loadStudents();
}, []);
if (loading) {
return (
<div className="text-center">
<div
className="spinner-border"
>
</div>
<p>
Loading students...
</p>
</div>
);
}
if (error) {
return (
<div className="alert alert-danger">
<h4>
Error
</h4>
<p>
{error}
</p>
</div>
);
}
if (students.length === 0) {
return (
<p>
No students found.
</p>
);
}
return (
<div>
<h2>
Student List
</h2>
{students.map(student => (
<div
key={student.id}
className="card mt-3"
>
<div className="card-body">
<h4>
{student.name}
</h4>
<p>
Course:
{student.course}
</p>
</div>
</div>
))}
</div>
);
}
export default Students;
try...catch...finally with async/await when suitable.Loading
↓
Request
↓
Success → Display Data
or
Loading
↓
Request
↓
Error → Display Error
useState can manage loading, error, and data states.fetch() can use catch() for rejected Promises.response.ok can be checked for HTTP errors.catch() and finally() for request handling.Question: Which state is commonly used to indicate that an API request is currently in progress?