Rado Milushev
Rado Milushev

Reputation: 284

React Native ListView - rowHasChanged doesn't fire

I am trying to implement an infinite scroll in React Native. Below is the source of the component:

var React = require('react-native');
var server = require('../server');
var Post = require('./Post');
var SwipeRefreshLayoutAndroid = require('./SwipeRefreshLayout');
var backEvent = null;
var lastPostId = "";
var isLoadingMore = false;
var isLoadingTop = false;
var onEndReachedActive = false;

var {
    StyleSheet,
    ListView,
    View,
    Text,
    Image,
    ProgressBarAndroid,
    BackAndroid
} = React;

class Stream extends React.Component {

constructor(props) {
    super(props);
    this.ds = new ListView.DataSource({
        rowHasChanged: (row1, row2) => {
            console.log("rowHasChenged FIRED!!");
            return false;
        }
    });

    this.state = {
        dataSource: this.ds.cloneWithRows(['loader']),
        hasStream: false,
        posts: []
    };

}

componentDidMount() {

    BackAndroid.addEventListener('hardwareBackPress', () => {
        this.props.navigator.jumpBack();
        return true;
    }.bind(this));

    server.getStream('', '', 15).then((res) => {

        lastPostId = res[res.length-1].m._id;

        this.setState({
            posts: res,
            hasStream: true,
            dataSource: this.ds.cloneWithRows(res)
        }, () => onEndReachedActive = true);
    })
}

onRefresh() {
    var posts = this.state.posts;
    var firstPost = posts[0].m._id;

    console.log(this.state.dataSource._rowHasChanged);

    isLoadingTop = true;

    server.getStream('', firstPost, 4000)
        .then(res => {
            console.log(posts.length);
             posts = res.concat(posts);
             console.log(posts.length);
             this.setState({
                dataSource: this.ds.cloneWithRows(posts),
                posts
             }, () => {
                this.swipeRefreshLayout && this.swipeRefreshLayout.finishRefresh();
                isLoadingTop = false;
             });
        }).catch((err) => {
            isLoadingTop = false;
        })

}

onEndReached(event) {

    if(!onEndReachedActive) return;

    if(this.state.loadingMore || this.state.isLoadingTop)return;
    isLoadingMore = true;
    var posts = this.state.posts;
    server.getStream(posts[posts.length-1].m._id, '', 15)
        .then(res => {
            console.log('received posts');
            posts = posts.concat(res);
            lastPostId = posts[posts.length-1].m._id;
            this.setState({
                dataSource: this.ds.cloneWithRows(posts),
                posts
            }, ()=>isLoadingMore = false);
        })
}

renderHeader() {
    return (
        <View style={styles.header}>
            <Text style={styles.headerText}>Header</Text>
        </View> 
    )
}

renderRow(post) {

    if(post === 'loader') {
        return (
            <ProgressBarAndroid 
                styleAttr="Large" 
                style={styles.spinnerBottom}/>
        )
    }

    let hasLoader = post.m._id === lastPostId;

    let loader = hasLoader ? 
        <ProgressBarAndroid 
            styleAttr="Large" 
            style={styles.spinnerBottom}/> : null;

    return (
        <View>
            <Post 
                post={post}/>
            {loader}
        </View> 
    )
}

render() {


    return (
                <ListView
                    style={styles.mainContainer}
                    dataSource={this.state.dataSource}
                    renderRow={this.renderRow.bind(this)}
                    onEndReached={this.onEndReached.bind(this)}
                    onEndReachedThreshold={1}
                    pageSize={15} />
    );
}
}

The problem is that whenever I append (or prepend) new data, the rowHasChanged method of the DataSource doesn't fire. It just re-renders every row, even tho nothing has changed (except the new data). Any idea why the method is bypassed?

Upvotes: 7

Views: 2326

Answers (4)

Rado Milushev
Rado Milushev

Reputation: 284

Edit: Pass a function to setState to avoid race conditions

I just figured it out. If you are having the same issue, check the point at which you change your state with the new dataSource. Mine was like this:

this.setState({
    dataSource: this.ds.cloneWithRows(posts)
});

Instead you should always use the dataSource from the previous state, like this:

this.setState(state => ({
    dataSource: state.dataSource.cloneWithRows(posts)
}))

Cheers!

Upvotes: 12

Yohanip
Yohanip

Reputation: 63

for anyone still having issue with rowHasChanged called but are still returning false the following snippets might help

the datasource is initialized like usual:

let ds = new ListView.DataSource ({

  rowHasChanged: (a, b) => {
    const changed = (a !== b)
    return changed
  }

})

this.data = []

this.state = {
  listDataSource: ds.cloneWithRows(this.data)
}

here is the function which will update a row

updateRow = (row, rowId, sectionId) => {

  // make a shallow clone from the stored data array
  let blob = this.data.concat()

  // modify the row, since we are using the triple equal operator, we need to make sure we are giving it a new object (new ref)
  blob[rowId] = Object.assign({}, blob[rowId], {label: blob[rowId].label + '..cape..deh'})

  // tell react to update the source
  this.setState({
    listDataSource: this.state.listDataSource.cloneWithRows(blob)
  }, () => {
    // we need to update our data storage here! after the state is changed
    this.data = blob
  })

}

Upvotes: 0

Ed of the Mountain
Ed of the Mountain

Reputation: 5459

I agree it seems to make sense that you should always use the dataSource from the previous state.

Yet when I setState this way, rowHasChanged gets called for all rows, however, rowHasChanged always returns false and no rows are rendered??? Why?

  // This is callback handler that the ListView DetailView will 
  // call when a ListView item is edited
  onChange(waypoint: Object){
    console.log('Callback: rowNumber= ', waypoint.rowNumber);
    console.log('        length(m)= ', waypoint.distance.meters);

    var itemListChanged = this.state.itemList;
    itemListChanged[waypoint.rowNumber-1] = waypoint;
    this.setState({
            dataSource: this.state.dataSource.cloneWithRows(itemListChanged),
    });
  },

If I setState this way, renderRow is called for all rows unconditionally without rowHasChanged ever being called. Which is correct?

this.setState({
  dataSource: ds.cloneWithRows(itemListChanged),
});

ListView, datasource, and react-native are a hard learning curve coming from C#/C/C++.

Upvotes: 1

Asim
Asim

Reputation: 360

this worked for me, hope this helps. I created a new dataSource and assigned the updated data to it on state change as follows:`

var dataSource = new ListView.DataSource(
  {rowHasChanged: (r1, r2) => ( r1 !== r2)});

  this.setState({ dataSource : dataSource.cloneWithRows(posts) });

Now, the new data is assigned and the view is rendered correctly. Note that posts array that is assigned now holds the updated data. Still wondering though if it's the best way to do it but it works!

Upvotes: 4

Related Questions