Popular Searches
Popular Course Categories
Popular Courses

Building an API-Integrated Application with Pagination and Search

Building an API-Integrated Application with Pagination and Search

API Integration and Network Requests

Building an API-Integrated Application with Pagination and Search in React Native

Course: React Native Training

Topic: Building an API-Integrated Application with Pagination and Search

Course Link: React Native Training at JustAcademy

Demo Registration: Register for a Course Demo

JustAcademy Website: Visit JustAcademy


1. Introduction

Modern mobile applications frequently retrieve information from remote APIs. Examples include product catalogs, employee directories, news feeds, customer lists, social media posts, and online learning platforms. As the amount of data grows, displaying every record on a single screen becomes inefficient and difficult to navigate.

API integration allows a React Native application to communicate with a backend service and retrieve dynamic data. Pagination divides a large dataset into smaller portions, while search helps users find specific records without manually browsing the entire list.

Combining API integration, pagination, and search makes mobile applications more responsive, scalable, and user-friendly.

1.1 What Is API Integration?

API integration is the process of connecting a mobile application to a remote service through an Application Programming Interface. The application sends requests, the server processes them, and the server returns data or a response indicating the outcome.

For example, a shopping application may request a list of products from an API. The API returns product information in JSON format, and React Native displays it using components such as FlatList, Text, Image, and View.

1.2 What Is Pagination?

Pagination is a technique for dividing a large collection of records into smaller groups called pages. Instead of downloading 10,000 records at once, an application might request 20 records at a time.

For example, a product list containing 1,000 products can be loaded in pages of 20 products. The first request retrieves products 1–20, the second retrieves products 21–40, and subsequent requests retrieve the remaining records.

1.3 What Is Search?

Search allows users to enter a keyword and retrieve records that match the entered text. Search may be implemented on the server, on the client, or through a combination of both approaches.

For a large remote dataset, server-side search is generally preferable because the application does not need to download every record before searching.

2. Learning Objectives

After completing this topic, learners should be able to:

  • Understand REST API integration in React Native.
  • Retrieve JSON data using the Fetch API.
  • Display remote data using FlatList.
  • Understand page-based, offset-based, and cursor-based pagination.
  • Implement load-more functionality using FlatList.
  • Create a search input with controlled state.
  • Send search keywords to an API.
  • Combine pagination and search without mixing results from different queries.
  • Handle loading indicators, errors, empty results, and retries.
  • Prevent duplicate requests and manage request cancellation.
  • Optimize list rendering and test the complete application.

3. Understanding the Application Architecture

An API-integrated application typically contains four important layers:

  1. User interface: Displays the search field, list of records, loading indicators, and error messages.
  2. State management: Stores the current results, search keyword, page number, loading state, and error information.
  3. API service: Sends requests to the backend and processes responses.
  4. Backend API: Validates request parameters, searches records, applies pagination, and returns data.

3.1 Application Data Flow

User opens the screen
        |
        v
React Native requests the first page
        |
        v
Backend returns JSON data
        |
        v
Application stores the results
        |
        v
FlatList displays the records
        |
        +---- User reaches the bottom
        |              |
        |              v
        |       Request the next page
        |              |
        |              v
        |       Append new records
        |
        +---- User enters a search keyword
                       |
                       v
               Request matching records
                       |
                       v
               Reset pagination
                       |
                       v
               Display search results

3.2 Example Use Cases

Application API Data Search Example Pagination Example
E-commerce Products Search for headphones Load 20 products at a time
Employee directory Employee records Search by employee name Load 25 employees per page
News application Articles Search by headline Load the next group of articles
Learning platform Courses Search for React Native Load 10 courses at a time
Social application Posts Search posts by keyword Load more posts while scrolling

4. Prerequisites and Project Setup

Before implementing the application, ensure that a React Native development environment is available. You should understand JavaScript functions, Promises, async/await, React components, useState, useEffect, and basic REST API concepts.

4.1 Important Components and APIs

Component or API Purpose
FlatList Efficiently renders a scrollable list of records.
TextInput Accepts search keywords from the user.
ActivityIndicator Displays loading feedback.
Pressable Creates interactive buttons and touchable items.
useState Stores data, query, pagination, and loading state.
useEffect Runs side effects such as an initial API request.
useRef Stores values such as the current request identifier without causing a render.
fetch Sends HTTP requests and receives server responses.
AbortController Cancels supported requests that are no longer needed.

5. Understanding REST APIs and JSON Responses

A REST API exposes resources through URLs and HTTP methods. For example, a products endpoint may return a collection of product records.

5.1 Example API Endpoint

GET https://example.com/api/products

An API may return JSON in a format such as:

{
  "items": [
    {
      "id": 1,
      "title": "Wireless Headphones",
      "price": 1999
    },
    {
      "id": 2,
      "title": "Bluetooth Speaker",
      "price": 1499
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 100,
  "hasMore": true
}

This response includes a collection of products and pagination metadata. The exact property names depend on the backend API; some APIs return products, results, data, or a different structure.

5.2 Making a Basic API Request

const response = await fetch(
  'https://example.com/api/products'
);

if (!response.ok) {
  throw new Error(`HTTP error: ${response.status}`);
}

const result = await response.json();
console.log(result);

Fetch returns a Promise. Checking response.ok is important because HTTP responses such as 404 and 500 do not automatically cause Fetch to reject its Promise.

6. Understanding Pagination Methods

Different APIs use different pagination strategies. The correct implementation depends on the parameters and metadata supported by the backend.

6.1 Page-Based Pagination

Page-based pagination uses a page number and a page size.

GET /api/products?page=1&limit=20
GET /api/products?page=2&limit=20
GET /api/products?page=3&limit=20

Here, page 1 retrieves the first group of records, page 2 retrieves the next group, and so on.

6.2 Offset-Based Pagination

Offset-based pagination specifies how many records to skip and how many records to retrieve.

GET /api/products?offset=0&limit=20
GET /api/products?offset=20&limit=20
GET /api/products?offset=40&limit=20

With a limit of 20, an offset of 40 skips the first 40 records and requests the next 20.

6.3 Cursor-Based Pagination

Cursor-based pagination uses a value provided by the server to identify where the next group of records begins. This is common in feeds and APIs that need stable traversal through changing datasets.

GET /api/posts?limit=20
GET /api/posts?limit=20&cursor=eyJpZCI6MjB9

The cursor value above is illustrative. Real cursor values should be taken from the API response rather than generated manually.

6.4 Pagination Comparison

Method Request Parameters Advantages Considerations
Page-based page, limit Simple to understand and implement. Results can shift if records are inserted or removed between requests.
Offset-based offset, limit Useful when an API supports direct offsets. Large offsets may become expensive on some backends.
Cursor-based cursor, limit Often works well for continuously changing feeds. Requires handling server-provided cursors and next-page metadata.

7. Understanding FlatList

FlatList is a React Native component designed to display scrollable lists efficiently. Unlike rendering every record inside a ScrollView, FlatList renders items in a virtualized list and is generally more suitable for large or dynamically changing datasets.

7.1 Basic FlatList Example

import React from 'react';
import { FlatList, Text, View } from 'react-native';

const products = [
  { id: '1', title: 'Headphones' },
  { id: '2', title: 'Keyboard' },
  { id: '3', title: 'Mouse' }
];

export default function ProductList() {
  return (
    <FlatList
      data={products}
      keyExtractor={item => item.id}
      renderItem={({ item }) => (
        <View>
          <Text>{item.title}</Text>
        </View>
      )}
    />
  );
}

7.2 Important FlatList Props

Prop Purpose
data Array of records to display.
renderItem Defines how each record is rendered.
keyExtractor Returns a stable, unique key for each item.
onEndReached Calls a function when the list approaches its end.
onEndReachedThreshold Controls how close to the end the callback can be triggered.
ListFooterComponent Displays content such as a load-more spinner at the bottom.
ListEmptyComponent Displays content when the list contains no records.
refreshing Controls the pull-to-refresh loading state.
onRefresh Starts a refresh operation when the user pulls down.

8. Implementing Page-Based Pagination

Page-based pagination requires state for the current page, the page size, the loaded records, whether another page exists, and whether a request is already running.

8.1 Pagination State Variables

const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const PAGE_SIZE = 20;

These variables have separate purposes:

  • items stores the records currently displayed.
  • page identifies the page to request next.
  • hasMore indicates whether additional records are available.
  • isLoading represents initial or full-list loading.
  • isLoadingMore represents the loading state for additional pages.

8.2 Building the API URL

const PAGE_SIZE = 20;

function buildProductsUrl(page, query) {
  const params = new URLSearchParams({
    page: String(page),
    limit: String(PAGE_SIZE)
  });

  if (query.trim()) {
    params.set('search', query.trim());
  }

  return `https://example.com/api/products?${params.toString()}`;
}

This example assumes the backend accepts page, limit, and search. If the backend uses different parameter names, change the function to match its API contract.

9. Implementing Search Functionality

Search starts with a TextInput that captures a keyword. When the keyword changes, the application can request matching records from the server.

9.1 Creating a Search Input

import React, { useState } from 'react';
import { TextInput, View } from 'react-native';

export default function SearchInputExample() {
  const [query, setQuery] = useState('');

  return (
    <View>
      <TextInput
        value={query}
        onChangeText={setQuery}
        placeholder="Search products..."
        autoCapitalize="none"
        autoCorrect={false}
        returnKeyType="search"
      />
    </View>
  );
}

9.2 Client-Side Search

Client-side search filters records that have already been downloaded. It can be appropriate for small, local datasets.

const filteredItems = items.filter(item =>
  item.title.toLowerCase().includes(query.trim().toLowerCase())
);

This approach only searches the records available in the local array. If the API contains thousands of records and only the first page has been downloaded, client-side search cannot find a matching record on a page that has not been fetched.

9.3 Server-Side Search

Server-side search sends the keyword to the API. The backend searches its complete dataset and returns matching records.

const params = new URLSearchParams({
  search: query.trim(),
  page: '1',
  limit: '20'
});

const response = await fetch(
  `https://example.com/api/products?${params.toString()}`
);

Server-side search is usually preferable for large datasets, frequently changing records, and applications that need consistent results across pages.

10. Combining Search with Pagination

Combining search and pagination requires careful state management. When the search keyword changes, the current page must normally reset to the first page. The application must also replace the previous results instead of appending search results to an unrelated list.

10.1 Expected Behavior

  1. The application loads page 1 of the complete dataset.
  2. The user enters a search keyword.
  3. The previous search request is cancelled or ignored if it is no longer relevant.
  4. The application resets pagination to page 1.
  5. The API returns the first page of matching records.
  6. The user scrolls toward the bottom of the list.
  7. The application requests the next page for the same keyword.
  8. The new records are appended to the existing search results.
  9. When no additional records are available, further pagination stops.

10.2 Why Resetting the Page Is Necessary

Suppose a user is viewing page 5 of all products and then searches for "keyboard". The keyboard search may contain only one page of results. If the application continues requesting page 6, the result will likely be empty or incorrect.

Therefore, a new search should reset the page number and replace the existing result set.

11. Complete API-Integrated Application with Search and Pagination

The following example demonstrates a product list using React Native, Fetch API, FlatList, server-side search, page-based pagination, loading indicators, pull-to-refresh, request cancellation, and error handling.

Important: The sample endpoint below uses JSONPlaceholder posts to demonstrate a working public API structure. It supports pagination using _page and _limit, but it does not implement genuine server-side keyword search. The code therefore filters each returned page locally for this demonstration. For a production search across the entire dataset, replace the endpoint and URL builder with a backend that supports a search parameter and returns pagination metadata.

import React, {
  useCallback,
  useEffect,
  useRef,
  useState
} from 'react';

import {
  ActivityIndicator,
  FlatList,
  Pressable,
  Text,
  TextInput,
  View
} from 'react-native';

const API_URL = 'https://jsonplaceholder.typicode.com/posts';
const PAGE_SIZE = 20;

export default function PostsSearchScreen() {
  const [items, setItems] = useState([]);
  const [query, setQuery] = useState('');
  const [isLoading, setIsLoading] = useState(true);
  const [isLoadingMore, setIsLoadingMore] = useState(false);
  const [isRefreshing, setIsRefreshing] = useState(false);
  const [error, setError] = useState('');
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);

  const queryRef = useRef('');
  const pageRef = useRef(1);
  const hasMoreRef = useRef(true);
  const loadingMoreRef = useRef(false);
  const requestIdRef = useRef(0);
  const controllerRef = useRef(null);

  const loadPage = useCallback(async ({
    requestedPage,
    searchText,
    mode = 'initial'
  }) => {
    const requestId = ++requestIdRef.current;

    controllerRef.current?.abort();
    const controller = new AbortController();
    controllerRef.current = controller;

    if (mode === 'more') {
      loadingMoreRef.current = true;
      setIsLoadingMore(true);
    } else if (mode === 'refresh') {
      setIsRefreshing(true);
    } else {
      setIsLoading(true);
    }

    setError('');

    try {
      const params = new URLSearchParams({
        _page: String(requestedPage),
        _limit: String(PAGE_SIZE)
      });

      const response = await fetch(
        `${API_URL}?${params.toString()}`,
        { signal: controller.signal }
      );

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      const result = await response.json();

      if (!Array.isArray(result)) {
        throw new Error('Unexpected API response');
      }

      if (requestId !== requestIdRef.current) {
        return;
      }

      const normalizedQuery = searchText.trim().toLowerCase();

      // Demo only: searches this page's records, not the entire server dataset.
      const visibleItems = normalizedQuery
        ? result.filter(item =>
            String(item.title ?? '')
              .toLowerCase()
              .includes(normalizedQuery)
          )
        : result;

      const nextHasMore = result.length === PAGE_SIZE;

      setItems(previous =>
        mode === 'more'
          ? [...previous, ...visibleItems]
          : visibleItems
      );

      const nextPage = requestedPage + 1;
      setPage(nextPage);
      pageRef.current = nextPage;

      setHasMore(nextHasMore);
      hasMoreRef.current = nextHasMore;
    } catch (err) {
      if (
        err.name !== 'AbortError' &&
        requestId === requestIdRef.current
      ) {
        setError('Unable to load posts. Please try again.');
      }
    } finally {
      if (requestId === requestIdRef.current) {
        setIsLoading(false);
        setIsLoadingMore(false);
        setIsRefreshing(false);
        loadingMoreRef.current = false;
      }
    }
  }, []);

  useEffect(() => {
    queryRef.current = query;
    pageRef.current = 1;
    hasMoreRef.current = true;

    setItems([]);
    setPage(1);
    setHasMore(true);

    loadPage({
      requestedPage: 1,
      searchText: query,
      mode: 'initial'
    });

    return () => {
      controllerRef.current?.abort();
    };
  }, [query, loadPage]);

  const loadMore = useCallback(() => {
    if (
      isLoading ||
      loadingMoreRef.current ||
      !hasMoreRef.current ||
      error
    ) {
      return;
    }

    loadPage({
      requestedPage: pageRef.current,
      searchText: queryRef.current,
      mode: 'more'
    });
  }, [error, isLoading, loadPage]);

  const refresh = useCallback(() => {
    pageRef.current = 1;
    hasMoreRef.current = true;

    setPage(1);
    setHasMore(true);

    loadPage({
      requestedPage: 1,
      searchText: queryRef.current,
      mode: 'refresh'
    });
  }, [loadPage]);

  if (isLoading && items.length === 0) {
    return (
      <View>
        <ActivityIndicator size="large" />
        <Text>Loading posts...</Text>
      </View>
    );
  }

  if (error && items.length === 0) {
    return (
      <View>
        <Text accessibilityRole="alert">{error}</Text>
        <Pressable onPress={refresh}>
          <Text>Try Again</Text>
        </Pressable>
      </View>
    );
  }

  return (
    <View>
      <TextInput
        value={query}
        onChangeText={setQuery}
        placeholder="Search posts by title..."
        autoCapitalize="none"
        autoCorrect={false}
        returnKeyType="search"
      />

      {error ? (
        <View>
          <Text accessibilityRole="alert">{error}</Text>
          <Pressable onPress={refresh}>
            <Text>Retry</Text>
          </Pressable>
        </View>
      ) : null}

      <FlatList
        data={items}
        keyExtractor={item => String(item.id)}
        renderItem={({ item }) => (
          <View>
            <Text>{item.title}</Text>
            <Text>{item.body}</Text>
          </View>
        )}
        onEndReached={loadMore}
        onEndReachedThreshold={0.4}
        refreshing={isRefreshing}
        onRefresh={refresh}
        ListEmptyComponent={
          !isLoading && !error ? (
            <Text>No matching posts found.</Text>
          ) : null
        }
        ListFooterComponent={
          isLoadingMore ? (
            <ActivityIndicator size="small" />
          ) : null
        }
      />
    </View>
  );
}

11.1 Understanding the Complete Example

Feature Implementation
Initial loading isLoading controls the initial indicator.
Data storage items stores records displayed by FlatList.
Search input query stores the current keyword.
Pagination page and hasMore track page progression.
Load more onEndReached triggers the next request.
Pull to refresh refreshing and onRefresh support refresh interaction.
Error handling try/catch records a user-friendly error.
Cleanup AbortController cancels an obsolete request.
Request tracking requestIdRef prevents an older request from replacing newer results.

11.2 Important Limitations of the Demo

  • The sample API does not perform global keyword search. The example filters only the records returned for the current page.
  • When filtering each page locally, a page may contain no matching records even though later pages do. This is why production search should generally be implemented by the backend.
  • The example uses page size and response length to estimate whether more data exists. Prefer explicit metadata such as hasMore, nextCursor, or a total count when the API provides it.
  • Some APIs return a total count header or a nested response object instead of a plain array. Adjust the response parsing accordingly.
  • In a production application, consider debouncing search input so that the app does not issue a request for every keystroke.

12. Implementing True Server-Side Search

For a real application, the backend should accept both the search keyword and pagination parameters. It should search the complete dataset, apply pagination to the matching records, and return consistent metadata.

12.1 Example Request

GET /api/products?search=headphones&page=1&limit=20

12.2 Example Response

{
  "items": [
    {
      "id": 101,
      "title": "Wireless Headphones",
      "price": 1999
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 1,
  "hasMore": false
}

12.3 API Service Function

const API_BASE_URL = 'https://example.com/api/products';

export async function searchProducts({
  query = '',
  page = 1,
  limit = 20,
  signal
}) {
  const params = new URLSearchParams({
    page: String(page),
    limit: String(limit)
  });

  if (query.trim()) {
    params.set('search', query.trim());
  }

  const response = await fetch(
    `${API_BASE_URL}?${params.toString()}`,
    {
      method: 'GET',
      headers: {
        Accept: 'application/json'
      },
      signal
    }
  );

  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }

  const result = await response.json();

  if (!Array.isArray(result.items)) {
    throw new Error('Invalid products response');
  }

  return {
    items: result.items,
    page: result.page,
    limit: result.limit,
    total: result.total,
    hasMore: Boolean(result.hasMore)
  };
}

This service function centralizes URL construction, request handling, response validation, and metadata extraction. It assumes the backend returns the documented object structure; change the parser to match the actual API.

13. Debouncing Search Input

When a user types "laptop", the input changes several times: "l", "la", "lap", "lapt", "lapto", and "laptop". Sending a request for every keystroke can increase server traffic and produce unnecessary work.

Debouncing delays a search operation until the user has stopped typing for a short period, such as 300 milliseconds.

13.1 Creating a Debounced Query

import { useEffect, useState } from 'react';

function useDebouncedValue(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

This custom hook delays updating the debounced value until the input has remained unchanged for the specified period.

13.2 Using the Debounced Query

const [query, setQuery] = useState('');
const debouncedQuery = useDebouncedValue(query, 300);

useEffect(() => {
  // Request the first page for debouncedQuery.
}, [debouncedQuery]);

Use the debounced query as the search value for the API request. Keep the input responsive by updating the visible TextInput immediately while delaying only the network request.

14. Handling Search and Pagination Race Conditions

A race condition can occur when two or more asynchronous requests finish in an unexpected order. For example, the user may search for "phone" and then quickly change the keyword to "tablet". If the earlier phone request completes last, it could overwrite the newer tablet results.

14.1 Common Causes

  • Search keywords change while previous requests are still running.
  • Users scroll to the bottom multiple times before a page request completes.
  • A refresh starts while another request is still in progress.
  • Network response times vary considerably.

14.2 Recommended Solutions

  • Cancel obsolete requests using AbortController where supported.
  • Track request identifiers and ignore responses from outdated requests.
  • Prevent simultaneous load-more requests with a loading flag or request lock.
  • Reset pagination when the search keyword changes.
  • Append records only when the response belongs to the current query and page.
  • Use stable record identifiers to prevent duplicate list items.

15. Preventing Duplicate Records

Some APIs may return duplicate records between pages, especially when the underlying dataset changes during pagination. Duplicate results can also appear if a load-more callback triggers more than once.

15.1 Deduplicating Records by ID

function appendUniqueItems(existingItems, newItems) {
  const existingIds = new Set(
    existingItems.map(item => item.id)
  );

  const uniqueNewItems = newItems.filter(
    item => !existingIds.has(item.id)
  );

  return [...existingItems, ...uniqueNewItems];
}

Use the function when appending a new page if the API does not guarantee unique records across responses. Ensure that IDs are consistently typed; if necessary, normalize them to strings before comparison.

16. Handling Loading, Empty, and Error States

A reliable application should provide appropriate feedback for every important request state.

State UI Behavior
Initial loading Show a loading indicator and a short explanation.
Loading more Display a small spinner at the bottom of the list.
Search in progress Keep the input responsive and show a subtle progress indicator when useful.
Successful results Display the records using FlatList.
Empty search result Show "No matching results found" and allow the user to change the keyword.
API error Display a friendly message and a Retry action.
End of dataset Stop requesting additional pages and optionally display an end-of-list message.
Refresh Reload the first page and reset the pagination state.

16.1 Example Empty State

<FlatList
  data={items}
  renderItem={renderItem}
  keyExtractor={item => String(item.id)}
  ListEmptyComponent={
    !isLoading && !error ? (
      <Text>No matching results found.</Text>
    ) : null
  }
/>

16.2 Example Footer Loading Indicator

<FlatList
  data={items}
  renderItem={renderItem}
  keyExtractor={item => String(item.id)}
  ListFooterComponent={
    isLoadingMore ? (
      <ActivityIndicator size="small" />
    ) : null
  }
/>

17. Optimizing Performance

Pagination reduces the amount of data fetched per request, while FlatList virtualizes list rendering. Additional optimizations can improve performance for large lists and frequent search operations.

17.1 Performance Best Practices

  • Choose a sensible page size based on the data and user experience.
  • Use FlatList rather than rendering very large arrays inside a ScrollView.
  • Provide a stable keyExtractor.
  • Keep list-item components reasonably small.
  • Use memoization for expensive row rendering when profiling shows a benefit.
  • Debounce server-side search input.
  • Cancel or ignore outdated requests.
  • Avoid unnecessary state updates and repeated API calls.
  • Use pagination metadata supplied by the backend.
  • Consider caching previously fetched results when appropriate.
  • Use cursor-based pagination for feeds where the backend supports it and changing data can make offsets unreliable.

17.2 Choosing a Page Size

Example Page Size Possible Use Trade-Off
10 Large product cards or detailed records. More frequent requests.
20 General-purpose mobile lists. Often a reasonable starting point.
50 Compact records with efficient APIs. More data is transferred per request.

These are illustrative values, not universal requirements. The best page size depends on payload size, server limits, network conditions, and the complexity of each list item.

18. Security and API Best Practices

  • Use HTTPS for API communication.
  • Do not store secret backend credentials in the mobile application bundle.
  • Use authentication tokens according to the backend's security requirements.
  • Never log passwords, tokens, or sensitive personal data.
  • Validate and sanitize search parameters on the backend.
  • Enforce authorization on the server rather than trusting the mobile client.
  • Apply rate limiting and reasonable page-size limits on the API.
  • Handle expired sessions and permission errors appropriately.
  • Do not assume that a hidden or disabled UI control provides security.
  • Validate API response structures before using them in components.

19. Common Problems and Troubleshooting

Problem Possible Cause Solution
List remains empty Incorrect URL or unexpected response shape. Inspect the response and update the parser.
Next page never loads Incorrect page value or load-more callback not triggered. Check pagination parameters and onEndReached settings.
Repeated API calls Multiple end-reached callbacks or changing effect dependencies. Use request guards and review dependencies.
Duplicate records appear Repeated page requests or overlapping API results. Deduplicate by a stable ID and prevent concurrent requests.
Search misses records Search filters only the loaded page. Implement server-side search across the complete dataset.
Old search results reappear An older request completes after a newer request. Cancel obsolete requests or use request identifiers.
Pagination resets unexpectedly Search or effect state is updated inconsistently. Keep query and pagination state synchronized.
Spinner never disappears Loading state is not reset on every completion path. Use finally and verify cleanup logic.
Next page contains no results Backend metadata is ignored or the page exceeds available results. Use hasMore, nextCursor, total count, or the API's documented pagination fields.
Search makes too many requests A request is sent on every keystroke. Debounce the search query and cancel outdated requests.
FlatList does not update Data is mutated instead of replaced with a new array. Use immutable state updates such as spread syntax.
API fails on a physical device Network restrictions, an incorrect host, TLS issues, or an unreachable server. Check the device network, server address, HTTPS configuration, and logs.

20. Testing the Application

Testing should cover API integration, search, pagination, and failure scenarios. A screen that works for the first page may still fail when the user searches quickly or repeatedly loads more records.

20.1 Manual Testing Checklist

  • Verify that the first page loads correctly.
  • Verify that the loading indicator appears during the initial request.
  • Scroll to the bottom and confirm that another page is loaded.
  • Verify that existing records remain visible while additional records load.
  • Search using a keyword that matches several records.
  • Search using a keyword that returns no results.
  • Clear the search and verify that the full list is restored.
  • Change the query rapidly and check that stale results do not replace the latest results.
  • Pull down to refresh and confirm that pagination resets.
  • Simulate a network error and verify that Retry works.
  • Verify that the final page stops further pagination.
  • Check that duplicate records are not appended.
  • Test with slow network conditions and large datasets.

20.2 Automated Testing Scenarios

  1. Mock a successful first-page API response and verify that records appear.
  2. Mock a second-page response and verify that new records are appended.
  3. Change the search query and verify that the request starts from page 1.
  4. Mock an empty result and verify that the empty-state message appears.
  5. Mock a failed request and verify that the error message is displayed.
  6. Simulate overlapping requests and verify that stale results are ignored.
  7. Verify that pagination stops when the backend reports no more results.

21. Mini Project: Product Search and Infinite Scrolling

Project Objective: Build a React Native application that retrieves products from a REST API, displays them in a scrollable list, supports server-side search, and loads additional pages as the user scrolls.

21.1 Functional Requirements

  • Fetch product information from a REST API.
  • Display each product's name, price, and image where available.
  • Implement a search TextInput.
  • Send search keywords to the backend.
  • Load the first page when the screen opens.
  • Load additional pages through FlatList's onEndReached callback.
  • Reset pagination when a new search is submitted.
  • Show initial and load-more indicators.
  • Implement pull-to-refresh.
  • Display appropriate empty and error states.
  • Prevent duplicate requests and stale search results.

21.2 Suggested Development Steps

  1. Create the React Native project and the product-list screen.
  2. Identify the API endpoint and confirm its response format.
  3. Create an API service function that accepts search, page, and page-size parameters.
  4. Build the initial loading and error states.
  5. Display product records using FlatList.
  6. Add a controlled TextInput for the search keyword.
  7. Debounce search input if requests should start automatically while typing.
  8. Reset the current page whenever the query changes.
  9. Implement onEndReached and guard against overlapping requests.
  10. Append new records and stop when the API indicates that no more pages exist.
  11. Add pull-to-refresh and retry behavior.
  12. Test empty results, failed requests, rapid search changes, and the final page.

21.3 Expected Results

User Action Expected Result
Opens the screen The first page is requested and a loading indicator appears.
Scrolls down The next page loads and new records are appended.
Enters a search term The list displays matching records from the new search.
Clears the search The default list is loaded again.
Search returns no results A clear empty-state message appears.
Network request fails A friendly error and retry option appear.
Reaches the last page No additional requests are made.

22. Practice Exercises

  1. Create a React Native screen that fetches users from a public REST API.
  2. Display the returned users using FlatList and stable keys.
  3. Implement page-based pagination using page and limit parameters.
  4. Implement offset-based pagination using offset and limit parameters.
  5. Create a search input that filters a small local array.
  6. Replace local filtering with a server-side search endpoint.
  7. Reset pagination when the search query changes.
  8. Add a loading indicator to the initial request and the load-more footer.
  9. Implement pull-to-refresh and reset the pagination state.
  10. Prevent duplicate load-more requests.
  11. Cancel or ignore outdated search requests.
  12. Handle empty results, API errors, and retry behavior.
  13. Deduplicate records when appending additional pages.
  14. Write automated tests for initial loading, search, and pagination.

23. Interview Questions and Answers

Q1. What is API integration in React Native?

API integration connects a React Native application to a remote service so that the app can send requests, retrieve data, and submit information.

Q2. What is pagination?

Pagination divides a large dataset into smaller groups of records, allowing the application to retrieve and display data incrementally.

Q3. Why is pagination important in mobile applications?

It reduces the amount of data retrieved in a single request, can reduce initial loading time and memory use, and makes large datasets easier to navigate.

Q4. What is the difference between page-based and offset-based pagination?

Page-based pagination identifies a page number and a page size. Offset-based pagination identifies how many records to skip and how many records to retrieve.

Q5. What is cursor-based pagination?

Cursor-based pagination uses a server-provided cursor to identify where the next batch of records should begin.

Q6. Why is FlatList preferred for long lists?

FlatList virtualizes list rendering and generally renders visible items more efficiently than placing a large number of records inside a ScrollView.

Q7. What does onEndReached do?

onEndReached calls a callback when the list scrolls close to the end of its content. It is commonly used to trigger pagination.

Q8. What is server-side search?

Server-side search sends the query to the backend, which searches the dataset and returns matching records. It is especially useful when the complete dataset is too large to download.

Q9. Why should pagination reset when a query changes?

The new query represents a different result set. Resetting the page ensures that the application starts from the beginning of the matching records rather than requesting an unrelated page number.

Q10. What is debouncing?

Debouncing delays an operation until a specified period has passed without a new input event. It reduces unnecessary search requests while the user types.

Q11. How can duplicate pagination requests be prevented?

Use a loading flag or request lock, check whether more records are available, and ignore repeated callbacks while the current request is running.

Q12. How do you prevent stale search results?

Cancel obsolete requests where supported or use request identifiers to ignore responses that no longer match the current query.

Q13. Why should API responses be validated?

Response validation ensures that the data has the expected structure before the application uses it. This helps prevent rendering errors when the backend returns unexpected data.

Q14. What is the purpose of hasMore?

hasMore indicates whether additional records are available. When the API reports that no further records exist, the application can stop requesting more pages.

Q15. What is the difference between initial loading and load-more loading?

Initial loading retrieves the first set of records, while load-more loading retrieves an additional page. A separate indicator for each state can prevent the existing list from disappearing during pagination.

Q16. How should an API error be handled?

Check the HTTP status, catch network and parsing errors, show a user-friendly message, and provide a retry action when the failure is recoverable.

Q17. What is a race condition in API search?

A race condition occurs when requests finish out of order and an older response overwrites results from a newer query.

Q18. How can duplicate records be removed?

Track stable item IDs and append only records whose IDs are not already present in the list.

Q19. When should cursor-based pagination be considered?

It is useful when the backend supports cursors and the dataset changes frequently, particularly for feeds where offset-based pages may shift.

Q20. How can API pagination be tested?

Test first-page loading, additional pages, final-page behavior, search changes, empty results, network failures, repeated callbacks, and out-of-order responses.

24. Quick Revision Checklist

  • Understand REST APIs, HTTP requests, and JSON responses.
  • Use Fetch API with async/await.
  • Check HTTP response status before processing data.
  • Use FlatList for large or dynamically changing lists.
  • Understand page-based, offset-based, and cursor-based pagination.
  • Use onEndReached to request additional pages.
  • Maintain separate initial-loading and load-more states.
  • Implement a controlled search input.
  • Prefer server-side search for large datasets.
  • Reset pagination whenever the query changes.
  • Debounce search input when appropriate.
  • Cancel or ignore outdated requests.
  • Prevent duplicate requests and duplicate records.
  • Handle empty results and errors.
  • Use API pagination metadata rather than guessing when possible.
  • Test the application under slow, failed, and rapidly changing requests.

25. Conclusion

Building an API-integrated application with pagination and search is an important React Native development skill. API integration makes dynamic server data available to the mobile interface, FlatList provides an efficient way to display records, pagination loads data incrementally, and search helps users locate relevant information quickly.

A robust implementation must also manage loading states, errors, empty results, query changes, duplicate requests, and outdated responses. Server-side search is especially important for large datasets because filtering only the currently loaded page can miss matching records.

By combining a well-designed API service, correct pagination logic, controlled search input, request cancellation, and appropriate testing, developers can build mobile applications that remain responsive and reliable as the amount of data grows.

26. Official Learning Resources

27. React Native Course and Demo Registration

Learn React Native fundamentals, API integration, state management, list rendering, and practical mobile application development through the course resources below.

Course Link: Explore React Native Training at JustAcademy

Demo Registration Link: Register for a Course Demo

General Website: Visit JustAcademy

whatsapp