React Native + Firebase Cloud Firestore: Implementing Infinite Scroll (Lazy Loading) With FlatList

By: Jeff Lewis

Jeff Lewis
Jul 1 · 6 min read

Notes:

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.

Although 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 (Depends on how much data there is).

2. Why Use Infinite Scroll (Lazy Loading)?

Apps such as Instagram, Facebook, Twitter, YouTube, Pinterest, and Medium use Infinite Scroll (Lazy Loading), so why is it used by big tech companies? Because 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 efficiently 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 on 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:

  1. data.json (Example Data)
  2. config.js (Firebase Config)
  3. App.js (React Native App)
  4. 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 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.

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.

Jeff Lewis

Written by

Full stack React/React Native developer, environmentalist, and beach bum.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade