When an API contains thousands of records, sending all records to a mobile application at once is not a good approach. Pagination divides the records into smaller pages and loads only the required records.
Pagination means dividing a large collection of records into smaller groups called pages.
Page 1 → Records 1 - 10
Page 2 → Records 11 - 20
Page 3 → Records 21 - 30
Page 4 → Records 31 - 40
The page parameter tells the API which page should be returned.
GET /api/students.php?page=1
For the second page:
GET /api/students.php?page=2
The limit parameter tells the API how many records should be returned on each page.
GET /api/students.php?page=1&limit=10
This request asks for 10 records from page 1.
GET /api/students.php?page=3&limit=10
This means:
Therefore, the API should return the third group of records.
MySQL uses LIMIT and OFFSET for pagination.
The offset formula is:
offset = (page - 1) * limit
For example:
page = 1
limit = 10
offset = (1 - 1) * 10
offset = 0
Page 1:
(1 - 1) * 10 = 0
Page 2:
(2 - 1) * 10 = 10
Page 3:
(3 - 1) * 10 = 20
Page 4:
(4 - 1) * 10 = 30
SELECT id, name, email
FROM students
ORDER BY id DESC
LIMIT 10 OFFSET 20;
This query returns 10 records starting from offset 20.
$page = max(
1,
(int)($_GET['page'] ?? 1)
);
$limit = max(
1,
(int)($_GET['limit'] ?? 10)
);
$offset = ($page - 1) * $limit;
This ensures that the page number is at least 1.
$stmt = $pdo->prepare(
"SELECT id, name, email
FROM students
ORDER BY id DESC
LIMIT :limit OFFSET :offset"
);
$stmt->bindValue(
':limit',
$limit,
PDO::PARAM_INT
);
$stmt->bindValue(
':offset',
$offset,
PDO::PARAM_INT
);
$stmt->execute();
$data = $stmt->fetchAll(
PDO::FETCH_ASSOC
);
The API needs to know the total number of records to calculate the number of available pages.
$countStmt = $pdo->query(
"SELECT COUNT(*) FROM students"
);
$total = (int)$countStmt->fetchColumn();
$totalPages = (int)ceil(
$total / $limit
);
For example, if there are 95 records and the limit is 10:
95 / 10 = 9.5
Total pages = 10
The API can return both records and pagination information.
{
"success": true,
"data": [
{
"id": 1,
"name": "Rahul Kumar"
}
],
"meta": {
"page": 1,
"limit": 10,
"total": 95,
"totalPages": 10
}
}
const [page, setPage] =
useState(1);
const [students, setStudents] =
useState([]);
const [loading, setLoading] =
useState(false);
const [hasMore, setHasMore] =
useState(true);
async function loadStudents(
pageNumber
) {
const response = await fetch(
`${API_URL}?page=${pageNumber}&limit=10`
);
const result =
await response.json();
return result;
}
useEffect(() => {
loadFirstPage();
}, []);
async function loadFirstPage() {
setLoading(true);
try {
const result =
await loadStudents(1);
setStudents(
result.data || []
);
setPage(1);
setHasMore(
result.meta.page <
result.meta.totalPages
);
} finally {
setLoading(false);
}
}
When the user reaches the end of the list, the next page can be requested.
const nextPage = page + 1;
const result =
await loadStudents(nextPage);
Pagination normally appends the new records to the existing records.
setStudents(prev => [
...prev,
...(result.data || [])
]);
The old records remain in the list and the new page is added after them.
FlatList provides onEndReached, which can be used to detect when the user reaches the end of the list.
<FlatList
data={students}
onEndReached={loadNextPage}
onEndReachedThreshold={0.5}
renderItem={({ item }) => (
<Text>
{item.name}
</Text>
)}
/>
The application should not request multiple pages at the same time.
async function loadNextPage() {
if (loading || !hasMore) {
return;
}
// Load next page...
}
This prevents unnecessary duplicate requests.
setHasMore(
result.meta.page <
result.meta.totalPages
);
When the current page reaches the last page, hasMore becomes false.
A footer spinner can inform the user that another page is loading.
ListFooterComponent={
loading ? (
<ActivityIndicator
size="small"
/>
) : null
}
Search and pagination can be combined.
GET /api/students.php
?search=rahul
&page=2
&limit=10
The server should first apply the search condition and then return the requested page.
Pagination can also work with sorting.
GET /api/students.php
?page=1
&limit=10
&sort=name
&order=asc
The PHP API should validate allowed sorting columns instead of directly inserting user input into an SQL ORDER BY clause.
<?php
header(
"Content-Type: application/json"
);
require_once "../db.php";
$page = max(
1,
(int)($_GET['page'] ?? 1)
);
$limit = max(
1,
min(
50,
(int)($_GET['limit'] ?? 10)
)
);
$offset =
($page - 1) * $limit;
$countStmt = $pdo->query(
"SELECT COUNT(*) FROM students"
);
$total =
(int)$countStmt->fetchColumn();
$totalPages = (int)ceil(
$total / $limit
);
$stmt = $pdo->prepare(
"SELECT id, name, email, course
FROM students
ORDER BY id DESC
LIMIT :limit OFFSET :offset"
);
$stmt->bindValue(
':limit',
$limit,
PDO::PARAM_INT
);
$stmt->bindValue(
':offset',
$offset,
PDO::PARAM_INT
);
$stmt->execute();
$data = $stmt->fetchAll(
PDO::FETCH_ASSOC
);
echo json_encode([
"success" => true,
"data" => $data,
"meta" => [
"page" => $page,
"limit" => $limit,
"total" => $total,
"totalPages" => $totalPages
]
]);
async function loadNextPage() {
if (loading || !hasMore) {
return;
}
setLoading(true);
try {
const nextPage =
page + 1;
const response =
await fetch(
`${API_URL}?page=${nextPage}&limit=10`
);
const result =
await response.json();
if (!response.ok) {
throw new Error(
result.message ||
"Unable to load data"
);
}
const newData =
result.data || [];
setStudents(prev => [
...prev,
...newData
]);
setPage(nextPage);
setHasMore(
result.meta.page <
result.meta.totalPages
);
} catch (error) {
console.log(
error.message
);
} finally {
setLoading(false);
}
}
Pagination can be combined with pull-to-refresh. Refreshing should normally clear the existing list and load page 1 again.
const [refreshing, setRefreshing] =
useState(false);
async function refreshStudents() {
setRefreshing(true);
try {
const result =
await loadStudents(1);
setStudents(
result.data || []
);
setPage(1);
setHasMore(
result.meta.page <
result.meta.totalPages
);
} finally {
setRefreshing(false);
}
}
import React, {
useEffect,
useState
} from "react";
import {
View,
Text,
FlatList,
ActivityIndicator
} from "react-native";
const API_URL =
"https://example.com/api/students.php";
export default function StudentList() {
const [students, setStudents] =
useState([]);
const [page, setPage] =
useState(1);
const [loading, setLoading] =
useState(false);
const [hasMore, setHasMore] =
useState(true);
useEffect(() => {
loadFirstPage();
}, []);
async function loadPage(
pageNumber
) {
const response =
await fetch(
`${API_URL}?page=${pageNumber}&limit=10`
);
const result =
await response.json();
if (!response.ok) {
throw new Error(
result.message ||
"Unable to load students"
);
}
return result;
}
async function loadFirstPage() {
setLoading(true);
try {
const result =
await loadPage(1);
setStudents(
result.data || []
);
setPage(1);
setHasMore(
result.meta.page <
result.meta.totalPages
);
} catch (error) {
console.log(
error.message
);
} finally {
setLoading(false);
}
}
async function loadNextPage() {
if (loading || !hasMore) {
return;
}
setLoading(true);
try {
const nextPage =
page + 1;
const result =
await loadPage(nextPage);
setStudents(prev => [
...prev,
...(result.data || [])
]);
setPage(nextPage);
setHasMore(
result.meta.page <
result.meta.totalPages
);
} catch (error) {
console.log(
error.message
);
} finally {
setLoading(false);
}
}
return (
<View style={{
flex: 1,
padding: 20
}}>
<FlatList
data={students}
keyExtractor={item =>
item.id.toString()
}
renderItem={({ item }) => (
<View style={{
padding: 15
}}>
<Text>
{item.name}
</Text>
<Text>
{item.email}
</Text>
<Text>
{item.course}
</Text>
</View>
)}
onEndReached={
loadNextPage
}
onEndReachedThreshold={
0.5
}
ListFooterComponent={
loading ? (
<ActivityIndicator
size="small"
/>
) : null
}
ListEmptyComponent={
!loading ? (
<Text>
No students found.
</Text>
) : null
}
/>
</View>
);
}
React Native
↓
FlatList
↓
onEndReached
↓
page + 1
↓
Fetch
↓
GET /students.php?page=2&limit=10
↓
PHP API
↓
Calculate OFFSET
↓
MySQL
↓
LIMIT + OFFSET
↓
JSON Response
↓
Append New Data
↓
FlatList
↓
Display More Records
Pagination is especially useful when building mobile applications that work with large student, product, customer, or transaction datasets.
Question: Which formula is used to calculate the OFFSET for API pagination?