Notes:

This guide assumes you know React Native and Firebase Cloud Firestore.

This guide also assumes you know how to import JSON data to Firebase Cloud Firestore. If you want to learn how to, check out my JSON To Firestore guide .

Github Repo: https://github.com/jefelewis/react-native-infinite-scroll-demo

1. What Is Infinite Scroll (Lazy Loading)

Infinite Scroll allows your app to query/retrieve data as needed. What that means is when a user scrolls down to the bottom of the list, an additional query is made and the data is appended.

There are isn’t a “Load More” or “Next Page” button, the query is automatically made in the background, which is where the name infinite scroll comes from. The user can scroll effortlessly and endlessly (assuming there is enough data to go on infinitely). It loads them when they reach a certain point on the page which is where the term “lazy load” comes from — it only loads it when you need it.

2. Why Use Infinite Scroll

Apps such as Instagram, Facebook, Twitter, YouTube, Pinterest, and Medium use infinite scroll, so why is it used by big tech companies? It’s a great user experience, and it efficiently queries only the data necessary and saves $$$ in data costs. In our case, we will be using Firebase Cloud Firestore, so infinite scroll will save in Firebase read costs.

For example, say you had a data set of 100 users, but our app can only show 9 users at a time. It would be inefficient to query Cloud Firestore for 100 users if your user never scrolls past the first 9 users. Infinite Scroll makes your app run much more efficient by querying as needed. It requires minimal effort from the user since fewer steps are needed because there are no page numbers and no buttons to click to query more data.

Not only is that inefficient in terms of Firebase read costs and the user’s experience, it’s also inefficient in terms of speed because it’s only sending your app the necessary data.

3. Example App + Code

Github Repo: https://github.com/jefelewis/react-native-infinite-scroll-test

A. App Overview

The App will only have a single screen (InfiniteScroll.js) and it will query from Firebase Cloud Firestore with a limit of 9 users. Once you scroll to the end of the list, 9 more users will be loaded (20 users total).

All of the data will be rendered in the React Native FlatList component.

B. App Screenshot

InfiniteScroll.js

C. App File Structure

This example will be using 4 files:

data.json (Example Data) config.js (Firebase Config) App.js (React Native App) InfiniteScroll.js (Infinite Scroll Screen)

D. App Files

A sample data.json file will be provided below. This data.json will need to be imported to Firebase Cloud Firestore.

If you are not sure how to import JSON data to Firebase Cloud Firestore, check the import JSON to Firestore guide.

data.json

{

"users": [

{

"id": 1,

"first_name": "Kristin",

"last_name": "Smith"

},

{

"id": 2,

"first_name": "Olivia",

"last_name": "Parker"

},

{

"id": 3,

"first_name": "Jimmy",

"last_name": "Robinson"

},

{

"id": 4,

"first_name": "Zack",

"last_name": "Carter"

},

{

"id": 5,

"first_name": "Brad",

"last_name": "Rayburn"

},

{

"id": 6,

"first_name": "Krista",

"last_name": "Foster"

},

{

"id": 7,

"first_name": "Parker",

"last_name": "Trotter"

},

{

"id": 8,

"first_name": "Kevin",

"last_name": "Carter"

},

{

"id": 9,

"first_name": "Fred",

"last_name": "Klein"

},

{

"id": 10,

"first_name": "Thomas",

"last_name": "Manchin"

},

{

"id": 11,

"first_name": "Taylor",

"last_name": "Welch"

},

{

"id": 12,

"first_name": "Sam",

"last_name": "Goldberg"

},

{

"id": 13,

"first_name": "John",

"last_name": "Russell"

},

{

"id": 14,

"first_name": "Steve",

"last_name": "Bell"

},

{

"id": 15,

"first_name": "Kelly",

"last_name": "Black"

},

{

"id": 16,

"first_name": "Lena",

"last_name": "Hunt"

},

{

"id": 17,

"first_name": "Jessica",

"last_name": "Moore"

},

{

"id": 18,

"first_name": "Pete",

"last_name": "Wong"

},

{

"id": 19,

"first_name": "Harry",

"last_name": "Fordham"

},

{

"id": 20,

"first_name": "Ashley",

"last_name": "Blake"

}

]

}

config.js

// Firebase Config

const firebaseConfig = {

apiKey: 'API_KEY_HERE',

authDomain: 'AUTH_DOMAIN_HERE',

databaseURL: 'DATABASE_URL_HERE',

projectId: 'PROJECT_ID_HERE',

storageBucket: 'STORAGE_BUCKET_HERE',

messagingSenderId: 'MESSAGING_ID_HERE',

}; // Exports

module.exports = firebaseConfig;

App.js

// Imports: Dependencies

import React from 'react';

import * as firebase from 'firebase';

import 'firebase/firestore';

import firebaseConfig from './config/config'; // Imports: Screens

import InfiniteScroll from './screens/InfiniteScroll'; // Firebase: Initialize

firebase.initializeApp({

apiKey: firebaseConfig.apiKey,

authDomain: firebaseConfig.authDomain,

databaseURL: firebaseConfig.databaseURL,

projectId: firebaseConfig.projectId,

storageBucket: firebaseConfig.storageBucket,

messagingSenderId: firebaseConfig.messagingSenderId,

}); // Firebase: Cloud Firestore

export const database = firebase.firestore(); // React Native: App

export default function App() {

return (

<InfiniteScroll />

);

}

InfiniteScroll.js

// Imports: Dependencies

import React, { Component } from 'react';

import { ActivityIndicator, Dimensions, FlatList, SafeAreaView, StyleSheet, Text, View } from 'react-native';

import { database } from '../App'; // Screen Dimensions

const { height, width } = Dimensions.get('window'); // Screen: Infinite Scroll

export default class InfiniteScroll extends React.Component {

constructor(props) {

super(props); this.state = {

documentData: [],

limit: 9,

lastVisible: null,

loading: false,

refreshing: false,

};

} // Component Did Mount

componentDidMount = () => {

try {

// Cloud Firestore: Initial Query

this.retrieveData();

}

catch (error) {

console.log(error);

}

}; // Retrieve Data

retrieveData = async () => {

try {

// Set State: Loading

this.setState({

loading: true,

});

console.log('Retrieving Data'); // Cloud Firestore: Query

let initialQuery = await database.collection('users')

.where('id', '<=', 20)

.orderBy('id')

.limit(this.state.limit) // Cloud Firestore: Query Snapshot

let documentSnapshots = await initialQuery.get(); // Cloud Firestore: Document Data

let documentData = documentSnapshots.docs.map(document => document.data()); // Cloud Firestore: Last Visible Document (Document ID To Start From For Proceeding Queries)

let lastVisible = documentData[documentData.length - 1].id; // Set State

this.setState({

documentData: documentData,

lastVisible: lastVisible,

loading: false,

});

}

catch (error) {

console.log(error);

}

}; // Retrieve More

retrieveMore = async () => {

try {

// Set State: Refreshing

this.setState({

refreshing: true,

});

console.log('Retrieving additional Data'); // Cloud Firestore: Query (Additional Query)

let additionalQuery = await database.collection('users')

.where('id', '<=', 20)

.orderBy('id')

.startAfter(this.state.lastVisible)

.limit(this.state.limit) // Cloud Firestore: Query Snapshot

let documentSnapshots = await additionalQuery.get(); // Cloud Firestore: Document Data

let documentData = documentSnapshots.docs.map(document => document.data()); // Cloud Firestore: Last Visible Document (Document ID To Start From For Proceeding Queries)

let lastVisible = documentData[documentData.length - 1].id; // Set State

this.setState({

documentData: [...this.state.documentData, ...documentData],

lastVisible: lastVisible,

refreshing: false,

});

}

catch (error) {

console.log(error);

}

}; // Render Header

renderHeader = () => {

try {

return (

<Text style={styles.headerText}>Items</Text>

)

}

catch (error) {

console.log(error);

}

}; // Render Footer

renderFooter = () => {

try {

// Check If Loading

if (this.state.loading) {

return (

<ActivityIndicator />

)

}

else {

return null;

}

}

catch (error) {

console.log(error);

}

}; render() {

return (

<SafeAreaView style={styles.container}>

<FlatList

// Data

data={this.state.documentData}

// Render Items

renderItem={({ item }) => (

<View style={styles.itemContainer}>

<Text>(ID: {item.id}) {item.first_name} {item.last_name}</Text>

</View>

)}

// Item Key

keyExtractor={(item, index) => String(index)}

// Header (Title)

ListHeaderComponent={this.renderHeader}

// Footer (Activity Indicator)

ListFooterComponent={this.renderFooter}

// On End Reached (Takes a function)

onEndReached={this.retrieveMore}

// How Close To The End Of List Until Next Data Request Is Made

onEndReachedThreshold={0}

// Refreshing (Set To True When End Reached)

refreshing={this.state.refreshing}

/>

</SafeAreaView>

)

}

} // Styles

const styles = StyleSheet.create({

container: {

height: height,

width: width,

},

headerText: {

fontFamily: 'System',

fontSize: 36,

fontWeight: '600',

color: '#000',

marginLeft: 12,

marginBottom: 12,

},

itemContainer: {

height: 80,

width: width,

borderWidth: .2,

borderColor: '#000',

justifyContent: 'center',

alignItems: 'center',

},

text: {

fontFamily: 'System',

fontSize: 16,

fontWeight: '400',

color: '#000',

},

});

Scroll Out

And that’s it! You have now implemented infinite scroll in your React Native app with Firebase Cloud Firestore, saving you $$$ in read costs and improving UX.

No one’s perfect. If you’ve found any errors, want to suggest enhancements, or expand on a topic, please feel free to send me a message. I will be sure to include any enhancements or correct any issues.