├── .watchmanconfig ├── assets ├── icon.png └── splash.png ├── babel.config.js ├── .gitignore ├── services └── jsonToFirestore.js ├── app.json ├── package.json ├── App.js ├── components ├── Title.js └── ItemSelector.js ├── README.md ├── data └── data.json └── screens └── InfiniteScroll.js /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jefelewis/react-native-infinite-scroll-demo/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jefelewis/react-native-infinite-scroll-demo/HEAD/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | node_modules/**/* 3 | .expo/* 4 | npm-debug.* 5 | *.jks 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | web-report/ 12 | 13 | config 14 | config/* 15 | config/config 16 | config/serviceAccount -------------------------------------------------------------------------------- /services/jsonToFirestore.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | const firestoreService = require('firestore-export-import'); 3 | const firebaseConfig = require('../config/config.js'); 4 | const serviceAccount = require('../config/serviceAccount.json'); 5 | 6 | // JSON To Firestore 7 | const jsonToFirestore = async () => { 8 | try { 9 | console.log('Initialzing Firebase'); 10 | await firestoreService.initializeApp(serviceAccount, firebaseConfig.databaseURL); 11 | console.log('Firebase Initialized'); 12 | 13 | await firestoreService.restore('./data/data.json'); 14 | console.log('Upload Success'); 15 | } 16 | catch (error) { 17 | console.log(error); 18 | } 19 | }; 20 | 21 | jsonToFirestore(); -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "React Native Infinite Scroll", 4 | "slug": "react-native-infinite-scroll", 5 | "privacy": "public", 6 | "sdkVersion": "33.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android", 10 | "web" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "expo": "^33.0.0", 12 | "firebase": "^6.2.4", 13 | "react": "16.8.3", 14 | "react-dom": "^16.8.6", 15 | "react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz", 16 | "react-native-vector-icons": "^6.5.0", 17 | "react-native-web": "^0.11.4", 18 | "react-navigation": "^3.11.0" 19 | }, 20 | "devDependencies": { 21 | "babel-preset-expo": "^5.1.1", 22 | "firestore-export-import": "^0.2.3" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | // Imports: Dependencies 2 | import React from 'react'; 3 | import { StyleSheet, Text, View } from 'react-native'; 4 | import * as firebase from 'firebase'; 5 | import 'firebase/firestore'; 6 | import firebaseConfig from './config/config'; 7 | 8 | // Imports: Screens 9 | import InfiniteScroll from './screens/InfiniteScroll'; 10 | 11 | // Firebase: Initialize 12 | firebase.initializeApp({ 13 | apiKey: `${firebaseConfig.apiKey}`, 14 | authDomain: `${firebaseConfig.authDomain}`, 15 | databaseURL: `${firebaseConfig.databaseURL}`, 16 | projectId: `${firebaseConfig.projectId}`, 17 | storageBucket: `${firebaseConfig.storageBucket}`, 18 | messagingSenderId: `${firebaseConfig.messagingSenderId}`, 19 | }); 20 | 21 | // Firebase: Cloud Firestore 22 | export const database = firebase.firestore(); 23 | 24 | // React Native: App 25 | export default function App() { 26 | return ( 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | // Styles 34 | const styles = StyleSheet.create({ 35 | container: { 36 | flex: 1, 37 | backgroundColor: '#fff', 38 | alignItems: 'center', 39 | justifyContent: 'center', 40 | }, 41 | }); -------------------------------------------------------------------------------- /components/Title.js: -------------------------------------------------------------------------------- 1 | 2 | // Imports: Dependencies 3 | import React, { Component } from "react"; 4 | import { Dimensions, View, SafeAreaView, StyleSheet, Text } from 'react-native'; 5 | 6 | // Screen Dimensions 7 | const { height, width } = Dimensions.get('window'); 8 | 9 | // Component: Title 10 | export default class Title extends Component { 11 | constructor(props) { 12 | super(props); 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 | 19 | {this.props.title} 20 | 21 | 22 | ) 23 | } 24 | } 25 | 26 | // Styles 27 | const styles = StyleSheet.create({ 28 | container: { 29 | height: 'auto', 30 | }, 31 | filterTitleContainer: { 32 | display: 'flex', 33 | flexDirection: 'row', 34 | alignItems: 'center', 35 | marginLeft: 16, 36 | marginRight: 16, 37 | marginTop: 60, 38 | marginBottom: 12, 39 | alignItems: 'flex-end', 40 | }, 41 | filterTitle: { 42 | alignSelf: 'flex-start', 43 | fontFamily: 'System', 44 | fontSize: 36, 45 | fontWeight: '700', 46 | color: '#000', 47 | }, 48 | }); -------------------------------------------------------------------------------- /components/ItemSelector.js: -------------------------------------------------------------------------------- 1 | // Imports: Dependencies 2 | import React, { Component } from "react"; 3 | import { Dimensions, Image, View, SafeAreaView, StyleSheet, Text, TouchableOpacity } from 'react-native'; 4 | import Icon from 'react-native-vector-icons/Ionicons'; 5 | 6 | // Screen Dimensions 7 | const { height, width } = Dimensions.get('window'); 8 | 9 | // Item Selector 10 | export default class ItemSelector extends Component { 11 | constructor(props) { 12 | super(props); 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 | 19 | 20 | {this.props.item.first_name} {this.props.item.last_name} 21 | 22 | 23 | ) 24 | } 25 | } 26 | 27 | // Styles 28 | const styles = StyleSheet.create({ 29 | container: { 30 | flexDirection: 'row', 31 | alignItems: 'center', 32 | width: width, 33 | height: 80, 34 | backgroundColor: '#fff', 35 | borderColor: '#D4D4D2', 36 | borderTopWidth: .2, 37 | }, 38 | arrowForward: { 39 | color: '#D4D4D2', 40 | opacity: .8, 41 | position: 'absolute', 42 | right: 20, 43 | }, 44 | titleText: { 45 | fontFamily: 'System', 46 | fontSize: 18, 47 | color: '#000', 48 | fontWeight: '400', 49 | marginLeft: 16, 50 | }, 51 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Infinite Scroll Demo 2 | * [Built With](#built-with) 3 | * [Pending Items](#pending-items) 4 | * [Color Scheme](#color-scheme) 5 | * [Screens](#screens) 6 | * [Getting Started](#getting-started) 7 | 8 | ## Built With 9 | * [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) - Programming Language 10 | * [React Native](https://facebook.github.io/react-native/) - Mobile (iOS/Android) Framework 11 | * [Expo](https://expo.io) - React Native Toolchain 12 | * [Firebase](https://firebase.google.com) - Database 13 | 14 | ## Pending Items 15 | 16 | ## Color Scheme 17 | * Blue: #007AFF 18 | * Grey: #7D7D7D 19 | * Light Grey: #E5E5E5 20 | 21 | ## Screens 22 | 23 | 24 | ## Getting Started 25 | **1. Connect Firebase:** 26 | In the root of your project, create a file called config.js. I've set up the .gitignore of this project to ignore the config.js file. **DO NOT COMMIT API KEYS TO GITHUB**. Copy and paste the **firebaseConfig** below into config.js to connect to Firebase. 27 | 28 | ``` 29 | // Firebase Config 30 | const firebaseConfig = { 31 | apiKey: 'YOUR_API_KEY_HERE', 32 | authDomain: 'YOUR_AUTH_DOMAIN_HERE', 33 | databaseURL: 'YOUR_DATABASE_URL_HERE', 34 | projectId: 'YOUR_PROJECT_ID_HERE', 35 | storageBucket: 'YOUR_STORAGE_BUCKET_HERE', 36 | messagingSenderId: 'YOUR_MESSAGING_SENDER_ID_HERE', 37 | appId: 'YOUR_APP_ID_HERE' 38 | } 39 | 40 | // Exports 41 | module.exports = firebaseConfig; 42 | ``` 43 | 44 | **2. Add .gitignore:** 45 | ``` 46 | node_modules 47 | node_modules/**/* 48 | .expo/* 49 | npm-debug.* 50 | *.jks 51 | *.p12 52 | *.key 53 | *.mobileprovision 54 | *.orig.* 55 | web-build/ 56 | web-report/ 57 | 58 | config 59 | config/* 60 | config/config 61 | config/serviceAccount 62 | ``` 63 | 64 | **3. Enable Firebase Cloud Firestore:** 65 | 1. Navigate to "Database" on the left sidebar 66 | 67 | 2. Click on "Create Database" 68 | 69 | 3. Select "Start in test mode" 70 | 71 | 4. Click on "Enable" 72 | 73 | **4. Install Dependencies:** 74 | ``` 75 | npm install 76 | ``` 77 | 78 | **5. Start iOS Simulator:** 79 | ``` 80 | expo start 81 | ``` 82 | -------------------------------------------------------------------------------- /data/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "id": 1, 5 | "first_name": "Kristin", 6 | "last_name": "Smith" 7 | }, 8 | { 9 | "id": 2, 10 | "first_name": "Olivia", 11 | "last_name": "Parker" 12 | }, 13 | { 14 | "id": 3, 15 | "first_name": "Jimmy", 16 | "last_name": "Robinson" 17 | }, 18 | { 19 | "id": 4, 20 | "first_name": "Zack", 21 | "last_name": "Carter" 22 | }, 23 | { 24 | "id": 5, 25 | "first_name": "Brad", 26 | "last_name": "Rayburn" 27 | }, 28 | { 29 | "id": 6, 30 | "first_name": "Krista", 31 | "last_name": "Foster" 32 | }, 33 | { 34 | "id": 7, 35 | "first_name": "Parker", 36 | "last_name": "Trotter" 37 | }, 38 | { 39 | "id": 8, 40 | "first_name": "Kevin", 41 | "last_name": "Carter" 42 | }, 43 | { 44 | "id": 9, 45 | "first_name": "Fred", 46 | "last_name": "Klein" 47 | }, 48 | { 49 | "id": 10, 50 | "first_name": "Thomas", 51 | "last_name": "Manchin" 52 | }, 53 | { 54 | "id": 11, 55 | "first_name": "Taylor", 56 | "last_name": "Welch" 57 | }, 58 | { 59 | "id": 12, 60 | "first_name": "Sam", 61 | "last_name": "Goldberg" 62 | }, 63 | { 64 | "id": 13, 65 | "first_name": "John", 66 | "last_name": "Russell" 67 | }, 68 | { 69 | "id": 14, 70 | "first_name": "Steve", 71 | "last_name": "Bell" 72 | }, 73 | { 74 | "id": 15, 75 | "first_name": "Kelly", 76 | "last_name": "Black" 77 | }, 78 | { 79 | "id": 16, 80 | "first_name": "Lena", 81 | "last_name": "Hunt" 82 | }, 83 | { 84 | "id": 17, 85 | "first_name": "Jessica", 86 | "last_name": "Moore" 87 | }, 88 | { 89 | "id": 18, 90 | "first_name": "Pete", 91 | "last_name": "Wong" 92 | }, 93 | { 94 | "id": 19, 95 | "first_name": "Harry", 96 | "last_name": "Fordham" 97 | }, 98 | { 99 | "id": 20, 100 | "first_name": "Ashley", 101 | "last_name": "Blake" 102 | } 103 | ] 104 | } -------------------------------------------------------------------------------- /screens/InfiniteScroll.js: -------------------------------------------------------------------------------- 1 | // Imports: Dependencies 2 | import React, { Component } from "react"; 3 | import { ActivityIndicator, Dimensions, FlatList, SafeAreaView, StyleSheet, Text, View } from 'react-native'; 4 | import { database } from '../App'; 5 | 6 | // Imports: Components 7 | import ItemSelector from '../components/ItemSelector'; 8 | import Title from '../components/Title'; 9 | 10 | // Screen Dimensions 11 | const { height, width } = Dimensions.get('window'); 12 | 13 | // Screen: InfiniteScroll 14 | export default class InfiniteScroll extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.state = { 19 | documentData: [], 20 | limit: 9, 21 | lastVisible: null, 22 | loading: false, 23 | refreshing: false, 24 | }; 25 | } 26 | 27 | // Component Did Mount 28 | componentDidMount = async () => { 29 | try { 30 | // Cloud Firestore: Initial Query (Infinite Scroll) 31 | this.retrieveData() 32 | } 33 | catch (error) { 34 | console.log(error); 35 | } 36 | } 37 | 38 | // Retrieve Data 39 | retrieveData = async () => { 40 | try { 41 | console.log('Retrieving Data'); 42 | 43 | // Set State: Loading 44 | this.setState({ loading: true }); 45 | 46 | // Cloud Firestore: Query 47 | let initialQuery = await database.collection('users') 48 | .where('id', '<=', 20) 49 | .orderBy('id') 50 | .limit(this.state.limit) 51 | 52 | // Cloud Firestore: Query Snapshot 53 | let documentSnapshots = await initialQuery.get(); 54 | 55 | // Cloud Firestore: Document Data 56 | let documentData = documentSnapshots.docs.map(document => document.data()); 57 | console.log('Document Data'); 58 | console.log(documentData); 59 | 60 | // Cloud Firestore: Last Visible Document (To Start From For Proceeding Queries) 61 | let lastVisible = documentData[documentData.length - 1].id; 62 | console.log('Last Visible ID'); 63 | console.log(lastVisible); 64 | 65 | // Set State 66 | this.setState({ 67 | documentData: documentData, 68 | lastVisible: lastVisible, 69 | loading: false, 70 | }); 71 | 72 | console.log('State: Document Data'); 73 | console.log(this.state.documentData); 74 | 75 | console.log('State: Last Visible ID'); 76 | console.log(this.state.lastVisible); 77 | 78 | console.log('State: Loading'); 79 | console.log(this.state.loading); 80 | } 81 | catch (error) { 82 | console.log(error); 83 | } 84 | }; 85 | 86 | // Retrieve More 87 | retrieveMore = async () => { 88 | try { 89 | console.log('Retrieving additional Data'); 90 | 91 | // Set State: Refreshing 92 | this.setState({ refreshing: true }); 93 | 94 | // Check If Last Visible Exists 95 | console.log('Retrieving More Last Visible'); 96 | console.log(this.state.lastVisible); 97 | 98 | // Cloud Firestore: Query (Additional Query) 99 | let additionalQuery = await database.collection('users') 100 | .where('id', '<=', 20) 101 | .orderBy('id') 102 | .startAfter(this.state.lastVisible) 103 | .limit(this.state.limit) 104 | 105 | // Cloud Firestore: Query Snapshot 106 | let documentSnapshots = await additionalQuery.get(); 107 | 108 | // Cloud Firestore: Document Data 109 | let documentData = documentSnapshots.docs.map(document => document.data()); 110 | console.log('Document Data'); 111 | console.log(documentData); 112 | 113 | // Cloud Firestore: Last Visible Document (To Start From For Proceeding Queries) 114 | let lastVisible = documentData[documentData.length - 1].id; 115 | console.log('Last Visible Id'); 116 | console.log(lastVisible); 117 | 118 | // Set State 119 | this.setState({ 120 | documentData: [...this.state.documentData, ...documentData], 121 | lastVisible: lastVisible, 122 | refreshing: false, 123 | }); 124 | } 125 | catch (error) { 126 | console.log(error); 127 | } 128 | }; 129 | 130 | // Render Header 131 | renderHeader = () => { 132 | try { 133 | return ( 134 | 135 | ) 136 | } 137 | catch (error) { 138 | console.log(error); 139 | } 140 | }; 141 | 142 | // Render Footer 143 | renderFooter = () => { 144 | try { 145 | // Check If Loading 146 | if (this.state.loading || this.state.refreshing) { 147 | // if (this.state.loading) { 148 | return ( 149 | <View style={styles.activityIndicator}> 150 | <ActivityIndicator /> 151 | </View> 152 | ) 153 | } 154 | else { 155 | return null; 156 | } 157 | } 158 | catch (error) { 159 | console.log(error); 160 | } 161 | }; 162 | 163 | // Select Item 164 | selectItem = (item) => { 165 | try { 166 | console.log(`Selected: ${item.first_name}`) 167 | } 168 | catch(error) { 169 | console.log(error); 170 | } 171 | }; 172 | 173 | render() { 174 | return ( 175 | <SafeAreaView style={styles.container}> 176 | <FlatList 177 | // Data 178 | data={this.state.documentData} 179 | // Render Items 180 | renderItem={({ item }) => ( 181 | <ItemSelector 182 | item={item} 183 | // onPress={() => {this.selectItem(item)}} 184 | /> 185 | )} 186 | // Element Key 187 | keyExtractor={(item, index) => String(index)} 188 | // Header (Title) 189 | ListHeaderComponent={this.renderHeader} 190 | // Footer (Activity Indicator) 191 | ListFooterComponent={this.renderFooter} 192 | // On End Reached (Takes in a function) 193 | onEndReached={this.retrieveMore} 194 | // How Close To The End Of List Until Next Data Request Is Made 195 | onEndReachedThreshold={0} 196 | // Refreshing (Set To True When End Reached) 197 | refreshing={this.state.refreshing} 198 | /> 199 | </SafeAreaView> 200 | ) 201 | } 202 | } 203 | 204 | // Styles 205 | const styles = StyleSheet.create({ 206 | container: { 207 | height: height, 208 | width: width, 209 | }, 210 | text: { 211 | fontFamily: 'System', 212 | fontSize: 16, 213 | fontWeight: '400', 214 | color: '#222222', 215 | }, 216 | }); --------------------------------------------------------------------------------