Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Native ListView: Wrong row removed when deleting

I know this has been asked before, see for example here, here and here.

However, none of the answers/comments are satisfactory. They either tell you to clone the data which doesn't solve the problem or to set a unique key on the ListView which solves the problem but creates a new one.

When you set a unique key on the ListView and update it after deletion the entire view renders again and scrolls to the top. A user who scrolls down a list to delete an item expects the list to maintain it's position.

Here's a contrived example, using what I would assume was the correct way of cloning the data:

var ListViewExample = React.createClass({

  getInitialState() {
    var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => {
      r1 !== r2
    }});
    var rows = ['row 1', 'row 2', 'row 3'];
    return {
      dataSource: ds.cloneWithRows(rows),
    };
  },

  _deleteRow() {
    var rows = ['row 1', 'row 3'];
    this.setState({dataSource: this.state.dataSource.cloneWithRows(rows)})
  },

  renderRow(rowData, sectionID, rowID) {
    return <TouchableOpacity onPress={this._deleteRow}
      style={{height: 60, flex: 1, borderBottomWidth: 1}}>
      <Text>{rowData}</Text>
    </TouchableOpacity>
  },

  render() {
    return (
      <ListView
        dataSource={this.state.dataSource}
        renderRow={this.renderRow}
      />
    );
  }

});

And here you can see a gif of it in action where the wrong row get's removed. The alternative, to set the key prop of the ListView does remove the correct row but as I said will cause the list to scroll to the top.

UPDATE

I've already accepted Nader's answer below but this does not seem to be the right way because it does not call rowHasChanged on the dataSource. It also doesn't conform to the ListView documentation. It did solve my problem so I'll leave it marked as the answer but I have the feeling this is a workaround rather than the correct solution

ANOTHER UPDATE

Okay this is awkward but my rowHasChanged function was missing the return statement. Too much ES6 sugar in my coffee I guess.

So in conclusion, if you screw up your rowHasChanged function, this._ds will work for some reason. If you do things the right way to begin with you should probably use something like:

this.setState({
  dataSource: this.state.dataSource.cloneWithRows(rows)
});

when updating your dataSource.

Remember also that you need a new rows datablob. Mutating it in place with something like cloneWithRows(rows.push(newRow) will not work whereas cloneWithRows(rows.concat([newRow]) will.

like image 454
Eysi Avatar asked Feb 15 '16 14:02

Eysi


1 Answers

I think the problem is that you are setting your datasource based on the previous ListView.DataSource instance. I've set up a demo of what I am talking about here, and put the example below along with another way of doing this.

Try doing this:

'use strict';

var React = require('react-native');
var {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
  ListView
} = React;

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

var SampleApp = React.createClass({

  getInitialState() {
    var rows = ['row 1', 'row 2', 'row 3'];
    return {
      dataSource: ds.cloneWithRows(rows),
    };
  },

  _deleteRow() {
    var rows = ['row 1', 'row 3'];
    this.setState({dataSource: ds.cloneWithRows(rows)})
  },

  renderRow(rowData, sectionID, rowID) {
    return <TouchableOpacity onPress={this._deleteRow}
      style={{height: 60, flex: 1, borderBottomWidth: 1}}>
      <Text>{rowData}</Text>
    </TouchableOpacity>
  },

  render() {
    return (
      <ListView
        dataSource={this.state.dataSource}
        renderRow={this.renderRow}
      />
    );
  }

});

https://rnplay.org/apps/YGBcIA

You can also use rowID in renderRow to identify the item you want to delete, then in your delete function, reset the state of the dataSource.

Check out the example I've set up here. Also, the code is below:

'use strict';

var React = require('react-native');
var {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
  ListView
} = React;

var rows = ['row 1', 'row 2', 'row 3'];
var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => {
      r1 !== r2
    }});

var SampleApp = React.createClass({

  getInitialState() {

    return {
      dataSource: ds.cloneWithRows([]),
      rows: rows
    };
  },

  componentDidMount() {
    this.setState({
        dataSource: ds.cloneWithRows( this.state.rows )
    })
  },

  _deleteRow(rowID) {
    this.state.rows.splice(rowID, 1)
    this.setState({
      dataSource: ds.cloneWithRows( this.state.rows ),
    })
  },

  renderRow(rowData, sectionID, rowID) {
    return <TouchableOpacity onPress={ () => this._deleteRow(rowID) }
      style={{height: 60, flex: 1, borderBottomWidth: 1}}>
      <Text>{rowData}</Text> 
    </TouchableOpacity>
  },

  render() {
    return (
      <ListView
        dataSource={this.state.dataSource}
        renderRow={this.renderRow}
      />
    );
  }

});
var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 28,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    fontSize: 19,
    marginBottom: 5,
  },
});

AppRegistry.registerComponent('SampleApp', () => SampleApp);
like image 124
Nader Dabit Avatar answered Sep 24 '22 09:09

Nader Dabit