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?
I think the problem is the
ListView
data source is not getting updated when you updateitem
. Theitem
passed totoggleItemEnabled
belongs tostate.datasource
whereas you are setting the datasource forprops.items
Solution 1
Clone the rows and set the rows again
Solution 2
This is the way I would prefer. I generally create a separate component for
Row
So you should refactor as followsCreate a new
Row.js
Then in your main component in render row method call this Row component