├── .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 |
150 |
151 |
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 |
176 | (
181 | {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 |
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 | });
--------------------------------------------------------------------------------