API requests take some time because data must travel between the React Native application and the server. During this time, the application should show a loading indicator so the user knows that the request is being processed.
A loading state tells the application that an API request is currently running.
Start Request
↓
Loading = true
↓
API Request
↓
Receive Response
↓
Loading = false
React Native can use useState to store the loading status.
const [loading, setLoading] =
useState(false);
Initially, loading is false because no API request is running.
Before starting an API request, set loading to true.
setLoading(true);
const response = await fetch(
API_URL
);
After the API request finishes, set loading back to false.
setLoading(true);
const response = await fetch(
API_URL
);
setLoading(false);
However, using finally is safer because it also handles errors.
setLoading(true);
try {
const response = await fetch(
API_URL
);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
The finally block runs whether the request succeeds or fails.
React Native provides ActivityIndicator for displaying a loading spinner.
import {
ActivityIndicator
} from "react-native";
Example:
<ActivityIndicator
size="large"
/>
{loading && (
<ActivityIndicator
size="large"
/>
)}
The indicator is displayed only when loading is true.
You can also show text along with the loading indicator.
{loading && (
<>
<ActivityIndicator
size="large"
/>
<Text>
Loading...
</Text>
</>
)}
async function loadStudents() {
setLoading(true);
try {
const response = await fetch(
API_URL
);
const result =
await response.json();
console.log(result);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
}
async function addStudent() {
setLoading(true);
try {
const response = await fetch(
API_URL,
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
name: "Rahul"
})
}
);
const result =
await response.json();
console.log(result);
} finally {
setLoading(false);
}
}
async function updateStudent(id) {
setLoading(true);
try {
const response = await fetch(
`${API_URL}?id=${id}`,
{
method: "PUT",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
name: "Rahul"
})
}
);
const result =
await response.json();
console.log(result);
} finally {
setLoading(false);
}
}
async function deleteStudent(id) {
setLoading(true);
try {
const response = await fetch(
`${API_URL}?id=${id}`,
{
method: "DELETE"
}
);
const result =
await response.json();
console.log(result);
} finally {
setLoading(false);
}
}
Loading can be used to prevent the same operation from being submitted multiple times.
async function handleSubmit() {
if (loading) {
return;
}
setLoading(true);
try {
// API request
} finally {
setLoading(false);
}
}
<Button
title={
loading
? "Saving..."
: "Save Student"
}
onPress={handleSubmit}
disabled={loading}
/>
The button becomes disabled while the request is running.
A real API screen commonly needs both loading and error states.
const [loading, setLoading] =
useState(false);
const [error, setError] =
useState("");
async function loadData() {
setLoading(true);
setError("");
try {
const response = await fetch(
API_URL
);
if (!response.ok) {
throw new Error(
"API request failed"
);
}
const result =
await response.json();
console.log(result);
} catch (error) {
setError(
error.message
);
} finally {
setLoading(false);
}
}
{error !== "" && (
<Text>
{error}
</Text>
)}
This allows the user to see an error when the API request fails.
A typical API screen can have three important states.
Loading
↓
Show Spinner
Success
↓
Show Data
Error
↓
Show Error Message
useEffect(() => {
loadStudents();
}, []);
The function can load API data when the component is first displayed.
const [students, setStudents] =
useState([]);
const [loading, setLoading] =
useState(false);
const [error, setError] =
useState("");
async function loadStudents() {
setLoading(true);
setError("");
try {
const response = await fetch(
API_URL
);
if (!response.ok) {
throw new Error(
"Unable to load students"
);
}
const result =
await response.json();
setStudents(
result.data || []
);
} catch (error) {
setError(
error.message
);
} finally {
setLoading(false);
}
}
if (loading) {
return (
<View>
<ActivityIndicator
size="large"
/>
<Text>
Loading students...
</Text>
</View>
);
}
if (error) {
return (
<View>
<Text>
{error}
</Text>
</View>
);
}
The UI can then display the data only when loading and error conditions have been handled.
Loading and error are not the only states. The API may successfully return an empty list.
if (
!loading &&
!error &&
students.length === 0
) {
return (
<Text>
No students found.
</Text>
);
}
Pull-to-refresh can use the same API loading logic.
async function refreshStudents() {
setLoading(true);
try {
await loadStudents();
} finally {
setLoading(false);
}
}
In larger applications, it is often better to keep one function responsible for the loading state rather than setting it in multiple places.
<FlatList
data={students}
keyExtractor={item =>
item.id.toString()
}
renderItem={({ item }) => (
<Text>
{item.name}
</Text>
)}
ListEmptyComponent={
loading
? <ActivityIndicator
size="large"
/>
: <Text>
No students found.
</Text>
}
/>
async function requestData() {
setLoading(true);
setError("");
try {
const response =
await fetch(API_URL);
if (!response.ok) {
throw new Error(
"Request failed"
);
}
return await response.json();
} catch (error) {
setError(
error.message
);
return null;
} finally {
setLoading(false);
}
}
import React, {
useEffect,
useState
} from "react";
import {
View,
Text,
ActivityIndicator,
FlatList
} from "react-native";
const API_URL =
"https://example.com/api/students.php";
export default function StudentList() {
const [students, setStudents] =
useState([]);
const [loading, setLoading] =
useState(false);
const [error, setError] =
useState("");
async function loadStudents() {
setLoading(true);
setError("");
try {
const response =
await fetch(API_URL);
if (!response.ok) {
throw new Error(
"Unable to load students"
);
}
const result =
await response.json();
if (!result.success) {
throw new Error(
result.message ||
"API request failed"
);
}
setStudents(
result.data || []
);
} catch (error) {
setError(
error.message
);
} finally {
setLoading(false);
}
}
useEffect(() => {
loadStudents();
}, []);
if (loading) {
return (
<View>
<ActivityIndicator
size="large"
/>
<Text>
Loading students...
</Text>
</View>
);
}
if (error) {
return (
<View>
<Text>
{error}
</Text>
</View>
);
}
return (
<FlatList
data={students}
keyExtractor={item =>
item.id.toString()
}
renderItem={({ item }) => (
<Text>
{item.name}
</Text>
)}
ListEmptyComponent={
<Text>
No students found.
</Text>
}
/>
);
}
Screen Opens
↓
Loading = true
↓
Fetch API
↓
┌───────────────┐
↓ ↓
Success Error
↓ ↓
Loading=false Loading=false
↓ ↓
Show Data Show Error
↓
If no data
↓
Show Empty State
A well-designed React Native API screen should clearly handle loading, success, error, and empty-data states.
Question: Which React Native component is commonly used to display a loading spinner?