Thanks to Facebook wall, Twitter, and other apps, we are getting very comfortable with infinite scrolls. With react native, we can easily replicate this in a mobile application.
What we are building
In this tutorial, we will build a list view with react native that supports infinite scroll. We will use Reddit API as our backend and get a listing of popular subreddits.
Prerequisites
For this tutorial, we are going to make use of the following:
- React native ListView
- Reddit’s subreddit API for the items
Fetching reddit API
To begin with the project, we’ll call reddit API to get the listing of the item. The URL is:
https://www.reddit.com/subreddits/popular/.json
If you want to play with the APIs, the complete listing of reddit API is located at https://www.reddit.com/dev/api/
For our application, we will use fetch library which is provided in react-native itself. Following is a snippet of code calling reddit api.
fetch('https://www.reddit.com/subreddits/popular/.json')
.then((response) => response.json())
.then((responseJson) => {
//deal with the data here
})
.catch((error) => {
console.error(error);
});
Creating the list view
Start off with creating view that shows a loading indicator. This view will then render a list view when data have been fetched.
import React, { Component } from "react";
import {
StyleSheet,
Text,
View,
ListView,
ActivityIndicator
} from "react-native";
export default class Application extends Component {
constructor(props) {
super(props);
this.state = {
dataSource: null,
isLoading: true
};
}
render() {
if (this.state.isLoading) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" />
</View>
);
} else {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={rowData => <Text>Item</Text>}
/>
);
}
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#F5FCFF"
}
});
To get the data, we need to load up reddit API and set the result as datasource for the list view.
Reddit will return data listing in the following format:
{
"kind": "Listing",
"data": {
"after": "t5_xxxx",
"before": "",
"modhash": "",
"children": [
//Individual item
]
}
}
Thus we need to grab data.children
to get the items.
export default class Application extends Component {
...
componentDidMount() {
//Start getting the first batch of data from reddit
fetch("https://www.reddit.com/subreddits/popular/.json")
.then(response => response.json())
.then(responseJson => {
let ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.setState({
dataSource: ds.cloneWithRows(responseJson.data.children),
isLoading: false
});
})
.catch(error => {
console.error(error);
});
}
...
}
When you load up the application now, it will be quite bland and all the items will be displayed as <Text>Item</Text>
Let’s fix this, add up a little more style and change the renderRow method to return a better styled list item.
export default class Application extends Component {
...
render() {
if (this.state.isLoading) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" />
</View>
);
} else {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={rowData => {
return (
<View style={styles.listItem}>
<View style={styles.imageWrapper}>
<Image
style={{ width: 70, height: 70 }}
source={{
uri: rowData.data.icon_img === ""
? "https://via.placeholder.com/70x70.jpg"
: rowData.data.icon_img
}}
/>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.title}>
{rowData.data.display_name}
</Text>
<Text style={styles.subtitle}>
{rowData.data.public_description}
</Text>
</View>
</View>
);
}}
/>
);
}
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#F5FCFF"
},
listItem: {
flex: 1,
flexDirection: "row",
borderBottomWidth: 1,
borderBottomColor: "#d6d7da",
padding: 6
},
imageWrapper: {
padding: 5
},
title: {
fontSize: 20,
textAlign: "left",
margin: 6
},
subtitle: {
fontSize: 10,
textAlign: "left",
margin: 6
}
});
Load more items on end
To get the infinite loader going, we need to know 2 things:
- When the list has been scrolled to the end. React native’s ListView already has the method
onEndReached
to handle this particular scenario. - What’s the next batch of item to load up. Reddit APIs also sends back
after
atrribute in their API result. This is the parameter we need to send to reddit to get the next batch of items.
To handle this mechanism, we need to add 2 more attributes to our state:
- isLoadingMore -> this is a boolean flag that will be set to true when we are loading additional item. This should be triggering an activity indicator at the footer of the list.
- _data -> to store the data that has been loaded. We should be concatenating the new data to this array of data
- _dataAfter -> to store the after parameter so we can pass this as parameter to load the next batch of data
Below, we extract the fetch code so we can reuse the same url while passing additional parameter.
export default class Application extends Component {
constructor(props) {
super(props);
this.fetchData = this._fetchData.bind(this);
...
}
_fetchData(callback) {
const params = this.state._dataAfter !== ""
? `?after=${this.state._dataAfter}`
: "";
fetch(`https://www.reddit.com/subreddits/popular/.json${params}`)
.then(response => response.json())
.then(callback)
.catch(error => {
console.error(error);
});
}
componentDidMount() {
//Start getting the first batch of data from reddit
this.fetchData(responseJson => {
let ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
const data = responseJson.data.children;
this.setState({
dataSource: ds.cloneWithRows(data),
isLoading: false
});
});
}
...
}
Next, let’s add the needed state and introduce fetchMore method to load more items
export default class Application extends Component {
constructor(props) {
super(props);
this.fetchMore = this._fetchMore.bind(this);
this.fetchData = this._fetchData.bind(this);
this.state = {
dataSource: null,
isLoading: true,
isLoadingMore: false,
_data: null,
_dataAfter: ""
};
}
_fetchData(callback) {
const params = this.state._dataAfter !== ""
? `?after=${this.state._dataAfter}`
: "";
fetch(`https://www.reddit.com/subreddits/popular/.json${params}`)
.then(response => response.json())
.then(callback)
.catch(error => {
console.error(error);
});
}
_fetchMore() {
this.fetchData(responseJson => {
const data = this.state._data.concat(responseJson.data.children);
this.setState({
dataSource: this.state.dataSource.cloneWithRows(data),
isLoadingMore: false,
_data: data,
_dataAfter: responseJson.data.after
});
});
}
...
}
Finally, we can handle the onEndReached
method by passing the control to our fetchMore
method.
Additionally, we can show ActivityIndicator
over at the list’s footer while waiting for more items to come.
export default class Application extends Component {
...
render() {
if (this.state.isLoading) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" />
</View>
);
} else {
return (
<ListView
...
onEndReached={() =>
this.setState({ isLoadingMore: true }, () => this.fetchMore())}
renderFooter={() => {
return (
this.state.isLoadingMore &&
<View style={{ flex: 1 }}>
<ActivityIndicator size="small" />
</View>
);
}}
/>
);
}
}
}
Final Result
That concludes the short tutorial to get an infinite scroll going with React Native.
Below is the code in action over at exponent. Let us know if this is useful for you in the comment, and enjoy!
P.S. We’re reducing the amount of data to fetch here so we can scroll to the end faster 😉