How to Test Pagination on React Native (Complete Guide)

Testing pagination on [React Native](https://reactnative.dev/docs/testing-overview) applications is crucial for ensuring a smooth, performant, and reliable user experience when dealing with large data

By · January 20, 2026 · 15 min read · How-To Guides

Testing pagination on React Native applications is crucial for ensuring a smooth, performant, and reliable user experience when dealing with large datasets. Pagination strategies, whether infinite scrolling, "Load More" buttons, or traditional numbered pages, introduce a complex interplay between UI rendering, network requests, state management, and user interaction. Without thorough testing, issues like duplicated content, missing items, incorrect loading indicators, performance bottlenecks, or even crashes can easily slip into production, degrading the user experience and potentially leading to data inconsistencies. This comprehensive guide will walk through the intricacies of testing pagination in React Native, covering everything from fundamental concepts and common failure modes to a detailed test matrix, manual testing techniques, and advanced automation strategies, including how autonomous testing platforms can uncover elusive bugs.

Understanding React Native Pagination Mechanisms and Their Failure Modes

Before diving into testing, it's essential to grasp the common pagination mechanisms in React Native and the specific ways they can fail. React Native apps often leverage FlatList, SectionList, or ScrollView components, combined with network requests, to implement pagination.

Common Pagination Implementations

  1. Infinite Scrolling (or "Scroll to Load More"): This is perhaps the most prevalent pattern. As the user scrolls near the end of the list, a new network request is triggered to fetch the next set of items, which are then appended to the existing list.
  1. "Load More" Button: Users explicitly tap a button at the end of the list to fetch more items.
  1. Traditional Paging (Numbered Pages): Less common in mobile apps, but still found. Users navigate explicitly between page numbers.

Why Pagination Breaks in Production

Pagination failures are insidious because they often depend on specific data volumes, network conditions, or user interaction patterns that are hard to replicate in development.

Crafting a Comprehensive Test Matrix for React Native Pagination

A robust test matrix is the backbone of thorough pagination testing. It ensures that all critical scenarios, from happy paths to obscure edge cases, are covered.

Test Matrix Structure

Our matrix will categorize tests by type: Functional, Performance, Accessibility, and Security. For each, we'll define specific test cases, expected outcomes, and potential failure points.

#### Functional Test Cases

These focus on the core behavior of the pagination mechanism.

Test Case CategorySpecific Test Case (Scenario)Expected OutcomePotential Failure Points
Initial LoadLoad list with 0 items.Empty state message displayed. No loading indicator.Incorrect empty state, loading indicator stuck.
Load list with < 1 page of items.All items displayed. No "Load More" button/infinite scroll trigger."Load More" button appears, infinite scroll attempts to fetch more.
Load list with exactly 1 full page of items.All items displayed. "Load More" button/infinite scroll trigger visible/active.Button not shown, infinite scroll not triggered.
Load list with > 1 page of items.First page of items displayed. "Load More" button/infinite scroll trigger visible/active.Incomplete first page, no trigger for more data.
Initial load with network error.Error message displayed. No items. "Retry" option available.Crash, indefinite loading, partial data.
Scrolling/LoadingInfinite scroll: Scroll to bottom of first page.Next page of items loaded and appended. Loading indicator shown during fetch, then hidden.Duplicated items, missing items, incorrect loading state, double fetches.
"Load More" button: Tap button.Next page of items loaded and appended. Button replaced with loading indicator, then re-enabled or hidden.Button stuck in loading, no new data, data out of order.
Scroll/Tap multiple times quickly (race condition).Items loaded in correct order, no duplicates. Only one request active at a time.Duplicated data, out-of-order items, multiple concurrent API calls.
Scroll/Tap when no more data is available.No new items loaded. "Load More" button hidden/disabled. Infinite scroll doesn't trigger. "End of list" message.Attempts to fetch empty data, button remains active.
Scroll/Tap when network error occurs during fetch.Error message displayed (e.g., "Failed to load more"). Existing items remain. "Retry" option.Crash, existing items cleared, infinite loading.
Fetch new page, then navigate away and back quickly.List state preserved (items, scroll position).List reset, items re-fetched from start.
App in background during fetch, then foregrounded.Fetch completes and updates UI correctly, or error handled if fetch was interrupted.Crash, corrupted UI, stale data.
Data IntegrityItems remain in correct order after multiple loads.Consistent order of items.Items re-ordered, duplicates appear.
No duplicate items after multiple loads.Each item appears exactly once.Duplicated unique IDs, visual duplicates.
Total number of items matches expected count after all pages loaded.Final item count is correct.Missing items, extra items.
Data displayed matches backend response (e.g., item details).Item content (text, images) is correct.Mismatched data due to caching or parsing errors.
Refresh/ResetPull-to-refresh (if implemented).List clears, first page re-fetched. Loading indicator shown.List not clearing, old data persists, refresh fails silently.
Navigate to another screen and return.List state (scroll position, loaded pages) preserved or reset based on design.Inconsistent behavior, unexpected data reload.
Search/Filter applied after loading multiple pages.List re-fetches based on new criteria, pagination resets.Search applies to already loaded data only, pagination state not reset.

#### Performance Test Cases

These focus on the responsiveness and efficiency of the pagination.

Test Case CategorySpecific Test Case (Scenario)Expected OutcomePotential Failure Points
RenderingLoad initial page (10-20 items).Smooth render, no UI jank. Time to render < 200ms.Janky UI, slow initial load.
Load subsequent pages (append 10-20 items).Smooth append, no UI jank. Time to append < 100ms.Janky scrolling, UI freezes during append.
Load a very large number of items (e.g., 500+).App remains responsive, scrolling is smooth.Significant jank, app freeze, OOM crash.
NetworkMonitor network requests during pagination.Only one request per page load. Request sizes are optimal.Multiple unnecessary requests, large payloads per request.
Test under slow network conditions (e.g., 3G, 2G emulation).App handles delays gracefully (loading indicators visible), no timeouts/crashes.App freezes, crashes, poor user experience.
Test under intermittent network conditions.App recovers from partial fetches or retries correctly.Data corruption, infinite loading.
Memory UsageLong session with many pagination loads.Memory usage remains stable or within acceptable limits.Memory leaks, OOM crashes over time.

#### Accessibility Test Cases (WCAG Compliance)

Ensuring pagination is usable by everyone, especially those with disabilities.

Test Case CategorySpecific Test Case (Scenario)Expected OutcomePotential Failure Points
Screen ReadersNavigate to "Load More" button with VoiceOver/TalkBack.Button is correctly labeled (e.g., "Load more items"), state announced (e.g., "disabled").Button not focusable, incorrect label, state not announced.
Announce loading indicator."Loading more items" announced clearly.Indicator not announced, or announced incorrectly.
Announce "End of list" message.Message is read out clearly.Message not announced, or announced too late.
Color Contrast"Load More" button and loading indicator text/spinner.Sufficient contrast for readability against background.Text/spinner blending into background.
Focus ManagementAfter loading more, focus remains logical (e.g., on first new item, or previous button).Predictable focus movement for keyboard navigation.Focus lost, jumps unexpectedly.

#### Security and Privacy Test Cases

While less common for pagination itself, these are important for the data being paginated.

Test Case CategorySpecific Test Case (Scenario)Expected OutcomePotential Failure Points
Data ExposureFetching paginated data from an unauthorized source.Request fails with appropriate error (e.g., 401, 403).Unauthorized access to data.
Paginated data does not expose sensitive information beyond user's permissions.Only permitted data fields are returned.Sensitive PII or business data exposed.
Rate LimitingRapid, repeated pagination requests.Backend rate limits requests, preventing abuse.Backend overloaded, denial of service.
Input ValidationIf pagination relies on client-side parameters (e.g., page=X), manipulate values.Invalid parameters are rejected by backend.SQL injection, unexpected data retrieval, server errors.

Manual Testing for React Native Pagination

Even with excellent automation, manual testing is indispensable for catching subtle UI/UX issues, performance jank, and context-dependent bugs.

Setup and Prerequisites

  1. Emulators/Devices: Test on a range of devices (various Android versions, iOS versions, screen sizes) to account for differences in rendering and performance.
  2. Network Throttling: Use developer tools (e.g., Chrome DevTools for Android, Network Link Conditioner for iOS, or react-native-debugger) to simulate slow and intermittent network conditions.
  3. Data Generation: Have access to test data that covers all pagination scenarios: 0 items, < 1 page, exactly 1 page, multiple pages, and a very large number of pages.
  4. Debugging Tools: Flipper, React Native Debugger, Xcode/Android Studio logs for monitoring network requests, component renders, and console output.

Step-by-Step Manual Test Scenarios

#### Scenario 1: Infinite Scrolling - Happy Path & Edge Cases

  1. Initial Load:
  1. Scroll to Load More:
  1. Rapid Scrolling:
  1. No More Data:
  1. Exactly One Page:
  1. < One Page:

#### Scenario 2: "Load More" Button - Happy Path & Edge Cases

  1. Initial Load:
  1. Tap "Load More":
  1. Rapid Tapping:
  1. No More Data:

#### Scenario 3: Error Handling & Network Conditions

  1. Network Offline:
  1. Slow Network:
  1. API Error (e.g., 500 Internal Server Error):
  1. Empty Response in Middle Page:

#### Scenario 4: State Management & Navigation

  1. Navigate Away and Back:
  1. Backgrounding/Foregrounding:

Automated Testing for React Native Pagination

Automation is critical for regression testing and covering a broader range of permutations quickly. We'll look at unit, integration, and E2E testing.

Unit Testing (Jest)

Unit tests focus on the logic of your pagination component and its underlying hooks/reducers, isolating them from the UI.


// src/hooks/usePaginatedData.js
import { useState, useEffect, useCallback } from 'react';

const usePaginatedData = (fetchFunction, initialPage = 1, pageSize = 10) => {
  const [data, setData] = useState([]);
  const [page, setPage] = useState(initialPage);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const [error, setError] = useState(null);

  const fetchData = useCallback(async () => {
    if (loading || !hasMore) return;

    setLoading(true);
    setError(null);

    try {
      const newData = await fetchFunction(page, pageSize);
      if (newData && newData.length > 0) {
        setData(prevData => [...prevData, ...newData]);
        setPage(prevPage => prevPage + 1);
        setHasMore(newData.length === pageSize); // Assume last page might have less than pageSize
      } else {
        setHasMore(false); // No more data
      }
    } catch (err) {
      setError(err);
      setHasMore(false); // On error, assume no more data for now
    } finally {
      setLoading(false);
    }
  }, [fetchFunction, page, pageSize, loading, hasMore]);

  const resetPagination = useCallback(() => {
    setData([]);
    setPage(initialPage);
    setHasMore(true);
    setLoading(false);
    setError(null);
  }, [initialPage]);

  useEffect(() => {
    // Initial fetch
    if (page === initialPage && data.length === 0 && hasMore && !loading) {
      fetchData();
    }
  }, [fetchData, page, initialPage, data.length, hasMore, loading]);


  return { data, loading, hasMore, error, fetchData, resetPagination };
};

export default usePaginatedData;

// src/hooks/__tests__/usePaginatedData.test.js
import { renderHook, act, waitFor } from '@testing-library/react-native';
import usePaginatedData from '../usePaginatedData';

describe('usePaginatedData', () => {
  const mockFetchFunction = jest.fn();

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should fetch initial data on mount', async () => {
    mockFetchFunction.mockResolvedValueOnce(['item1', 'item2']);
    const { result } = renderHook(() => usePaginatedData(mockFetchFunction, 1, 2));

    expect(result.current.loading).toBe(true);
    await waitFor(() => expect(result.current.loading).toBe(false));

    expect(mockFetchFunction).toHaveBeenCalledWith(1, 2);
    expect(result.current.data).toEqual(['item1', 'item2']);
    expect(result.current.page).toBe(2);
    expect(result.current.hasMore).toBe(true);
  });

  it('should fetch more data when fetchData is called', async () => {
    mockFetchFunction
      .mockResolvedValueOnce(['item1', 'item2']) // Initial load
      .mockResolvedValueOnce(['item3', 'item4']); // Second page

    const { result } = renderHook(() => usePaginatedData(mockFetchFunction, 1, 2));

    await waitFor(() => expect(result.current.loading).toBe(false)); // Initial load complete
    expect(result.current.data).toEqual(['item1', 'item2']);

    act(() => {
      result.current.fetchData();
    });

    expect(result.current.loading).toBe(true);
    await waitFor(() => expect(result.current.loading).toBe(false));

    expect(mockFetchFunction).toHaveBeenCalledWith(2, 2);
    expect(result.current.data).toEqual(['item1', 'item2', 'item3', 'item4']);
    expect(result.current.page).toBe(3);
    expect(result.current.hasMore).toBe(true);
  });

  it('should set hasMore to false when no more data is available', async () => {
    mockFetchFunction
      .mockResolvedValueOnce(['item1']) // Initial load (less than page size)
      .mockResolvedValueOnce([]); // No more data

    const { result } = renderHook(() => usePaginatedData(mockFetchFunction, 1, 2));

    await waitFor(() => expect(result.current.loading).toBe(false));
    expect(result.current.data).toEqual(['item1']);
    expect(result.current.hasMore).toBe(false); // Should be false after first fetch if less than page size

    act(() => {
      result.current.fetchData(); // Try to fetch again
    });

    // Should not call fetchFunction again if hasMore is false
    expect(mockFetchFunction).toHaveBeenCalledTimes(1);
    expect(result.current.loading).toBe(false);
    expect(result.current.data).toEqual(['item1']);
    expect(result.current.hasMore).toBe(false);
  });

  it('should handle network errors', async () => {
    const error = new Error('Network error');
    mockFetchFunction.mockRejectedValueOnce(error);

    const { result } = renderHook(() => usePaginatedData(mockFetchFunction));

    await waitFor(() => expect(result.current.loading).toBe(false));

    expect(result.current.error).toBe(error);
    expect(result.current.data).toEqual([]);
    expect(result.current.hasMore).toBe(false); // Assume no more data on error
  });

  it('should reset pagination state', async () => {
    mockFetchFunction
      .mockResolvedValueOnce(['item1', 'item2'])
      .mockResolvedValueOnce(['item3', 'item4']);

    const { result } = renderHook(() => usePaginatedData(mockFetchFunction, 1, 2));

    await waitFor(() => expect(result.current.loading).toBe(false));
    act(() => { result.current.fetchData(); });
    await waitFor(() => expect(result.current.loading).toBe(false));

    expect(result.current.data.length).toBe(4);
    expect(result.current.page).toBe(3);

    act(() => {
      result.current.resetPagination();
    });

    expect(result.current.data).toEqual([]);
    expect(result.current.page).toBe(1);
    expect(result.current.hasMore).toBe(true);
    expect(result.current.loading).toBe(false);
    expect(result.current.error).toBe(null);
  });
});

Integration Testing (React Native Testing Library)

Integration tests focus on how your pagination component interacts with its props, state, and child components, simulating user interaction.


// src/components/PaginatedList.js
import React from 'react';
import { FlatList, Text, ActivityIndicator, View, Button, StyleSheet } from 'react-native';
import usePaginatedData from '../hooks/usePaginatedData';

const PaginatedList = ({ fetchItems, renderItem, listHeaderComponent, emptyListComponent, pageSize = 10, initialPage = 1 }) => {
  const { data, loading, hasMore, error, fetchData, resetPagination } = usePaginatedData(fetchItems, initialPage, pageSize);

  const renderFooter = () => {
    if (loading) {
      return <ActivityIndicator style={styles.footer} size="large" />;
    }
    if (error) {
      return (
        <View style={styles.footer}>
          <Text style={styles.errorText}>Error: {error.message}</Text>
          <Button title="Retry" onPress={fetchData} />
        </View>
      );
    }
    if (!hasMore && data.length > 0) {
      return <Text style={styles.footer}>- End of list -</Text>;
    }
    // Only show "Load More" button if it's not infinite scroll, or if we want explicit control
    // For infinite scroll, onEndReached will handle it.
    // This example assumes infinite scroll, so no explicit "Load More" button in footer.
    return null;
  };

  const onEndReached = () => {
    if (!loading && hasMore && !error) {
      fetchData();
    }
  };

  const renderEmpty = () => {
    if (loading) {
      return null; // Don't show empty state while loading initially
    }
    if (error && data.length === 0) {
      return (
        <View style={styles.emptyContainer}>
          <Text style={styles.errorText}>Failed to load items.</Text>
          <Button title="Retry" onPress={resetPagination} />
        </View>
      );
    }
    return emptyListComponent || <Text style={styles.emptyContainer}>No items found.</Text>;
  };

  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={(item, index) => item.id || String(index)}
      onEndReached={onEndReached}
      onEndReachedThreshold={0.5} // When 50% of the list is scrolled, trigger fetch
      ListFooterComponent={renderFooter}
      ListHeaderComponent={listHeaderComponent}
      ListEmptyComponent={renderEmpty()}
      refreshing={false} // Add pull-to-refresh if needed, e.g., onRefresh={resetPagination}
    />
  );
};

const styles = StyleSheet.create({
  footer: {
    paddingVertical: 20,
    borderTopWidth: 1,
    borderTopColor: '#CED0CE',
    justifyContent: 'center',
    alignItems: 'center',
  },
  errorText: {
    color: 'red',
    textAlign: 'center',
    marginBottom: 10,
  },
  emptyContainer: {
    flex:

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 11 real users — finds bugs, accessibility violations, and security issues. No scripts. New to the category? Start with what autonomous product intelligence & QA means.

Try SUSA Free