Lesson 107 of 158 – React Native API Pagination
68%

React Native API Pagination

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.

Note: In this lesson, we will learn how to implement pagination with React Native + Fetch + PHP + MySQL + REST API + FlatList.

1. What is Pagination?

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

2. Why Pagination is Important in Mobile Apps

  • Reduces the amount of data downloaded.
  • Improves application performance.
  • Reduces server response size.
  • Uses less memory on the mobile device.
  • Makes large lists easier to manage.
  • Improves user experience.

3. Page Parameter

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

4. Limit Parameter

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.

5. Page and Limit Together

GET /api/students.php?page=3&limit=10

This means:

  • Page = 3
  • Records per page = 10

Therefore, the API should return the third group of records.

6. Calculating OFFSET

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

7. Pagination Calculation Examples

Page 1:
(1 - 1) * 10 = 0

Page 2:
(2 - 1) * 10 = 10

Page 3:
(3 - 1) * 10 = 20

Page 4:
(4 - 1) * 10 = 30

8. MySQL LIMIT and OFFSET

SELECT id, name, email
FROM students
ORDER BY id DESC
LIMIT 10 OFFSET 20;

This query returns 10 records starting from offset 20.

9. Reading Pagination Parameters in PHP

$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.

10. Using PDO for Pagination

$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
);

11. Counting Total Records

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();

12. Calculating Total Pages

$totalPages = (int)ceil(
    $total / $limit
);

For example, if there are 95 records and the limit is 10:

95 / 10 = 9.5

Total pages = 10

13. Pagination JSON Response

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
    }
}

14. React Native Page State

const [page, setPage] =
    useState(1);

const [students, setStudents] =
    useState([]);

const [loading, setLoading] =
    useState(false);

const [hasMore, setHasMore] =
    useState(true);

15. Fetching a Page from React Native

async function loadStudents(
    pageNumber
) {

    const response = await fetch(
        `${API_URL}?page=${pageNumber}&limit=10`
    );

    const result =
        await response.json();

    return result;
}

16. Loading the First Page

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);

    }
}

17. Loading the Next Page

When the user reaches the end of the list, the next page can be requested.

const nextPage = page + 1;

const result =
    await loadStudents(nextPage);

18. Appending New Records

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.

19. FlatList onEndReached

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>
    )}
/>

20. Preventing Duplicate Page Requests

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.

21. Checking hasMore

setHasMore(
    result.meta.page <
    result.meta.totalPages
);

When the current page reaches the last page, hasMore becomes false.

22. Pagination Footer Loader

A footer spinner can inform the user that another page is loading.

ListFooterComponent={
    loading ? (
        <ActivityIndicator
            size="small"
        />
    ) : null
}

23. Pagination with Search

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.

24. Pagination with Sorting

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.

25. Complete PHP Pagination API

<?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
    ]
]);

26. Complete React Native Pagination Function

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);

    }
}

27. Pull to Refresh

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);

    }
}

28. Complete React Native Pagination Example

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>
    );
}

29. Common Pagination Mistakes

  • Loading all records at once.
  • Using an incorrect offset calculation.
  • Forgetting to increment the page number.
  • Replacing old records instead of appending new records.
  • Making duplicate requests while a page is loading.
  • Not checking whether another page exists.
  • Not returning total page information from the API.
  • Not handling network errors.
  • Not showing a loading indicator.
  • Ignoring search and sorting when calculating pagination.
  • Not limiting the maximum page size.

30. Complete Pagination Flow

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.

📌 Key Points

  • Pagination divides large datasets into smaller pages.
  • Use page to identify the requested page.
  • Use limit to control records per page.
  • The OFFSET formula is (page - 1) × limit.
  • MySQL uses LIMIT and OFFSET for pagination.
  • Use PDO prepared statements for database queries.
  • Return pagination metadata from the API.
  • React Native can use FlatList's onEndReached.
  • Append new records to the existing list.
  • Prevent duplicate requests while loading.
  • Use hasMore to determine whether more records are available.
  • Pagination can be combined with search, sorting, filtering, and authentication.

🧠 Quick Quiz

Question: Which formula is used to calculate the OFFSET for API pagination?