I've been building a fun iOS application with React Native.

I wanted to have my main component's ListView render more than just the current day's list of products from Product Hunt's API. Their API lets me make requests based on either a certain day or X previous days. That part's easy (we just keep a counter of the number of days we've gone back, and increment it by one every time we need to make another call).

The challenging part was how to section off these lists -- continually updating our ListView, separate them by sections based on the date, etc.

Facebook's documentation on this is almost nonexistent. It referenced the need to use cloneWithRowsAndSections as opposed to cloneWithRows, but no documentation was present on how cloneWithRowsAndSections worked. This seemed like a great time to get familiar with the source code for ListViewDataSource.

Here's some interesting tidbits:

  • cloneWithRowsAndSections accepts three parameters: A dataBlob which can be any type of data structure, sectionIdentities which is an optional array of strings, and rowIdentities which is another optional array of strings.

  • cloneWithRows actually returns cloneWithRowsAndSections({s1: dataBlob}, ['s1'], rowIds). It places the data you pass it into a hash with the key of s1, which is also the name of the section. Essentially, cloneWithRows is just cloneWithRowsAndSections, but with only one section.

Alright, let's see how I implemented this:

First, in my getInitialState lifecycle event:

  getInitialState: function() {

    return {
      accessToken: this.props.accessToken,
      currentDay: 0,
      dataBlob: {},
      dataSource: new ListView.DataSource({
        rowHasChanged: (r1, r2) => r1 !== r2,
        sectionHeaderHasChanged: (s1, s2) => s1 !== s2
        }),
      loaded: false
    }
  },

The important parts of this Component's state are dataBlob and dataSource.

Here's the API call:

  getAllPosts: function() {

    api.getAllPosts(this.state.accessToken, this.state.currentDay)
      .then((responseData) => {
        var tempDataBlob = this.state.dataBlob;
        var date = new Date(responseData.posts[0].day).toDateString();
        tempDataBlob[date] = responseData.posts;
        this.setState({
          currentDay: this.state.currentDay + 1,
          dataBlob: tempDataBlob
        });
        ;
      }).then(() => {
        this.setState({
          dataSource: this.state.dataSource.cloneWithRowsAndSections(this.state.dataBlob),
          loaded: true
        })
      })
      .done();
  },

The important parts here are that I save the contents of this.state.dataBlob to a temporary variable, grab the date (I'm using the date as my section header), use the date as the key to store my responseData, and save it to state.

Then, I just use cloneWithRowsAndSections passing in this.state.dataBlob.

Now when I render ListView, I'll pass in a couple of props:

  renderListView: function() {
    return (
      <ListView
        dataSource={this.state.dataSource}
        renderRow={this.renderPostCell}
        renderSectionHeader={this.renderSectionHeader}
        renderFooter={this.renderFooter}
        onEndReached={() => {this.getAllPosts(this.state.currentDay)}}
        onEndReachedThreshold={40}
        style={styles.postsListView} />
      )
  },

renderSectionHeader just deals with the header itself. Section Headers are sticky (they stay at the top of the container once scrolled past, until another Section Header below it takes over):

  renderSectionHeader: function(sectionData, sectionID) {
    return (
      <View style={styles.section}>
        <Text style={styles.sectionText}>{sectionID}</Text>
      </View>
      )
  },

While renderFooter works in tandem with onEndReached and onEndReachedThreshold. The first just renders an ActivityIndicatorIOS spinning component:

  renderFooter: function() {
    return (
      <View>
        <ActivityIndicatorIOS
          animating={true}
          size={'large'} />
      </View>)
  },

While onEndReached dictates that another API call is made once we reach the end, to grab the previous day's content, and onEndReachedThreshold controls how far down we have to scroll for it to consider us at the "end".

Having ActivityIndicatorIOS rendered as my ListView footer gives a visual cue that data is being loaded, and since there is only one total ListView the footer just moves to the bottom of the now longer list once the API call finishes running.

Here's how it looks visually: