When a React Native application communicates with an API, the request may take some time or may fail. A professional application should handle these situations clearly.
Two important states are loading and error. Loading tells the user that an operation is in progress, while error handling tells the user when an operation could not be completed.
Start
↓
Loading
↓
API Request
↓
Success → Display Data
↓
Error → Display Error Message
A loading state represents an operation that is currently in progress.
For example, while an application is requesting users from an API, the screen can show a loading indicator.
Loading...
Please wait.
This gives the user feedback that the application is working.
Network requests do not always complete immediately. Without loading feedback, the user may think that the application is not responding.
The useState hook can be used to store loading status.
const [loading, setLoading] =
useState(false);
When a request starts:
setLoading(true);
When the request finishes:
setLoading(false);
React Native provides the ActivityIndicator component for
showing a loading indicator.
import {
ActivityIndicator
} from "react-native";
It can be displayed while the application is loading data.
<ActivityIndicator />
The loading state can be used to conditionally display the indicator.
{loading ? (
<ActivityIndicator />
) : (
<Text>
Data Loaded
</Text>
)}
When loading is true, the indicator is displayed.
An error state represents a failed operation.
For example, an API request can fail because of:
An error message can be stored using useState.
const [error, setError] =
useState("");
When an error occurs:
setError(
"Unable to load data"
);
The try...catch statement can handle errors in asynchronous
code.
try {
const response =
await fetch(url);
} catch (error) {
console.log(error);
}
If the request throws an error, the catch block runs.
try {
const response =
await fetch(url);
if (!response.ok) {
throw new Error(
"Request failed"
);
}
} catch (error) {
setError(
"Unable to load data"
);
}
Checking response.ok allows the application to handle
unsuccessful HTTP responses explicitly.
try {
const response =
await axios.get(url);
setData(response.data);
} catch (error) {
setError(
"Unable to load data"
);
}
Axios rejects the Promise for certain request failures, making
try...catch useful for error handling.
A screen can maintain both loading and error states.
const [loading, setLoading] =
useState(true);
const [error, setError] =
useState("");
The application can then display the appropriate UI based on these states.
Start Request
↓
setLoading(true)
↓
API Request
↓
Success?
/ \
Yes No
↓ ↓
Data Error
↓ ↓
Show Show
Data Message
\ /
↓
setLoading(false)
The finally block runs after the try or catch block.
It is useful for stopping a loading indicator.
try {
const response =
await axios.get(url);
} catch (error) {
setError(
"Request failed"
);
} finally {
setLoading(false);
}
When starting a new request, clear an old error message.
setError("");
setLoading(true);
This prevents an old error message from remaining visible during a new request.
{error ? (
<Text>
{error}
</Text>
) : null}
The error message is displayed only when an error value exists.
When a network request fails, the user can be given an option to try again.
<Button
title="Retry"
onPress={loadData}
/>
The Retry button can call the same function that loads the data.
if (loading) {
return (
<ActivityIndicator />
);
}
if (error) {
return (
<View>
<Text>
{error}
</Text>
<Button
title="Retry"
onPress={loadData}
/>
</View>
);
}
Loading and error states are not the only states an application should consider. An API can successfully return an empty list.
if (!loading &&
!error &&
data.length === 0) {
return (
<Text>
No data found.
</Text>
);
}
This provides a useful message when there is no data to display.
After a successful request, the application can display the returned data.
if (!loading &&
!error) {
return (
<Text>
Data Loaded Successfully
</Text>
);
}
A complete UI may therefore have loading, error, empty and success states.
API Screen
|
├── Loading
|
├── Error
|
├── Empty
|
└── Success
| State | Example |
|---|---|
| Loading | Loading... |
| Error | Unable to load data |
| Empty | No courses found |
| Success | Course list |
useEffect(() => {
loadData();
}, []);
The data loading function can be called when the component is mounted.
The function should update loading, data and error states appropriately.
A network request may fail because the device cannot communicate with the server.
try {
const response =
await fetch(url);
} catch (error) {
setError(
"Please check your internet connection."
);
}
The message should be understandable to the user.
A server may return an unsuccessful HTTP status.
const response =
await fetch(url);
if (!response.ok) {
throw new Error(
"Server request failed"
);
}
The application can then display an appropriate error state.
Authentication screens should also provide useful error feedback.
try {
await axios.post(
"/api/login",
loginData
);
} catch (error) {
setError(
"Invalid login details"
);
}
The exact message should reflect what the backend reports and what is safe to reveal to the user.
A loading state can also help prevent repeated actions while a request is in progress.
<Button
title={
loading
? "Loading..."
: "Load Data"
}
disabled={loading}
onPress={loadData}
/>
Disabling an action during a request can help avoid accidental duplicate requests.
import React, {
useEffect,
useState
} from "react";
import {
View,
Text,
Button,
ActivityIndicator,
FlatList
} from "react-native";
import axios from "axios";
export default function CoursesScreen() {
const [courses, setCourses] =
useState([]);
const [loading, setLoading] =
useState(true);
const [error, setError] =
useState("");
async function loadCourses() {
try {
setLoading(true);
setError("");
const response =
await axios.get(
"https://example.com/api/courses"
);
setCourses(
response.data
);
} catch (error) {
setError(
"Unable to load courses."
);
} finally {
setLoading(false);
}
}
useEffect(() => {
loadCourses();
}, []);
if (loading) {
return (
<View>
<ActivityIndicator />
<Text>
Loading courses...
</Text>
</View>
);
}
if (error) {
return (
<View>
<Text>
{error}
</Text>
<Button
title="Retry"
onPress={loadCourses}
/>
</View>
);
}
if (courses.length === 0) {
return (
<Text>
No courses found.
</Text>
);
}
return (
<FlatList
data={courses}
keyExtractor={
item =>
item.id.toString()
}
renderItem={({ item }) => (
<View>
<Text>
{item.name}
</Text>
</View>
)}
/>
);
}
This example handles loading, error, empty and success states in one screen.
| Concept | Purpose |
|---|---|
| loading | Indicates that an operation is in progress |
| error | Stores or represents a failed operation |
| ActivityIndicator | Displays a visual loading indicator |
| try...catch | Handles errors in asynchronous operations |
| finally | Runs after the try or catch block |
| Retry | Allows the user to attempt a failed request again |
| Empty State | Handles successful requests with no data |
| Success State | Displays successfully loaded data |
Open Screen
↓
Start API Request
↓
setLoading(true)
↓
Show Loading UI
↓
Send Request
↓
Result
/ \
Success Failure
↓ ↓
Data Error
↓ ↓
Check Set Error
Empty? ↓
↓ Error UI
Show Data ↓
↓ Retry
setLoading(false)
↓
Final UI State
Loading and error handling are essential when building React Native applications that communicate with APIs. A good screen should clearly handle loading, success, empty and error states so that users always understand what is happening.
Question: Which React Native component is commonly used to display a loading indicator?