How to force ReactNative ListView to update a row when row-data change?

5.6k views Asked by At

ReactNative's ListView is little bit odd when it comes to data updates.

I have an array of objects (name: Item), that have 3 properties:

  • id (int)
  • name (string)
  • enabled (bool)

The array of items is displayed in a ListView. Each row shows the name of the item and a checkmark representing the enabled-state (red on true, gray on false). In my example it is simplified to a simple string "true"/"false".

Each row is embedded in a TouchableOpacity with a onPress-EventHandler, that toggles the enabled-state of the represented item.

Unfortunately, the ListView won't update the row.

While debugging the rowHasChanged-Event, it turns out that the rowHasChanged-Event could never determine the changed state because both rows already get the new state after toggling the enabled-property without any update to the UI.

Here is my Code:

The Item-Class: Item.js

export default class Item {

  constructor(id, name, enabled) {
    this._id = id;
    this._name = name;
    this._enabled = enabled;
  }

  get id() {return this._id}

  get name() {return this._name}
  set name(value) {this._name = value}

  get enabled() {return this._enabled}
  set enabled(value) {this._enabled = value}
  
}

The ListView:

'use strict';

import React, {PropTypes, Component} from 'react';

import {View,TouchableOpacity, TouchableHighlight,
  Text, ListView, StyleSheet} from 'react-native';

import Item from './Item';

class ItemListView extends Component {

  constructor(props) {
    super(props);

    this.updateListView = this.updateListView.bind(this);
    this.toggleItemEnabled = this.toggleItemEnabled.bind(this);

    this.state = {
      dataSource: new ListView.DataSource({
                rowHasChanged: (row1, row2) => row1 != row2
            }),
    };
  }

  componentDidMount() {
    this.updateListView(this.props.items)
  }

  componentWillReceiveProps(nextProps) {
    this.updateListView(nextProps.items)
    }

  updateListView(items) {
    this.setState({
            dataSource: this.state.dataSource.cloneWithRows(
                items.slice() // copy items to a new array
            ),
        });
  }

  toggleItemEnabled(item) {
    item.enabled = !item.enabled;

    this.updateListView(this.props.items);
  }

  renderItem(item) {
    console.log('Render', item.enabled, item.name)
    return (
      <TouchableOpacity style={{flex:1}} onPress={ () => this.toggleItemEnabled(item) }>
        <View style={s.row}>
              <Text>{item.name + ' ' + item.enabled}</Text>
          </View>
      </TouchableOpacity>
    )
  }

  render() {
    return (
      <View style={s.container}>
        <ListView
          style={s.listView}
          dataSource={this.state.dataSource}
          renderRow={(item) => this.renderItem(item)}
        />
      </View>
    )
  }

}

ItemListView.propTypes = {
  items: PropTypes.array
};

ItemListView.defaultProps = {
  items: [],
};

export default ItemListView;


const s = StyleSheet.create({
  container: {
  },

  row: {
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginTop: 12,
    paddingBottom: 12,
    paddingHorizontal: 30,
    borderBottomWidth: 1,
    borderBottomColor: 'gray',
  },

  innerContainer: {
    alignItems: 'center',
  },

});

How to fix this behaviour, that the ListView gets updated when row-data changes?

2

There are 2 answers

1
coder hacker On BEST ANSWER

I think the problem is the ListView data source is not getting updated when you update item. The item passed to toggleItemEnabled belongs to state.datasource whereas you are setting the datasource for props.items

Solution 1

Clone the rows and set the rows again

toggleItemEnabled(item, sectionId, rowId) {
    newItems = this.props.items.slice();
    newItems[rowId].enabled = !newItems[rowId].enabled;

    this.updateListView(newItems);
}

Solution 2

This is the way I would prefer. I generally create a separate component for Row So you should refactor as follows

Create a new Row.js

constructor(props) {
    super(props);

    this.state = {
       enabled: false;
    };
 }


toggleItemEnabled() {
    this.setState({enabled: !this.state.enabled});

}

render() {
 item = this.props.data;
 return (
      <TouchableOpacity style={{flex:1}} onPress={ () => this.toggleItemEnabled() }>
        <View style={s.row}>
              <Text>{item.name + ' ' + this.state.enabled}</Text>
          </View>
      </TouchableOpacity>
    )

}

Then in your main component in render row method call this Row component

renderItem(item) {
     return(
         <Row data={item} />
     );
 }
0
ayed abboushi On
toggleItem(item){
    item.isSelected = !item.isSelected;
    const items = [...this.state.dataSource];
    const index = items.findIndex((x) => x.id === item.id);
    items[index] = item;
    this.setState({ dataSource: items });
}