React Native – creating infinite scroll listview

By w s / July 29, 2017
react native infinite scroll

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.

React Native Infinite Scroll

React Native Infinite Scroll – Final Result

Prerequisites

For this tutorial, we are going to make use of the following:

  1. React native ListView
  2. 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:

  1. When the list has been scrolled to the end. React native’s ListView already has the method onEndReached to handle this particular scenario.
  2. 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:

  1. 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.
  2. _data -> to store the data that has been loaded. We should be concatenating the new data to this array of data
  3. _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 😉