41

I have an iOS app I am making with react-native. The Game class contains a ListView component. I set the state in the constructor and include a dataSource. I have a hardcoded array of data for right now that I store in a different state property (this.state.ds). Then in the componentDidMount I use the cloneWithRows method to clone my this.state.ds as my dataSource for the view. That is pretty standard as far as ListViews go and is working well. Here is the code:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 */
'use strict';

 var React = require("react-native");
 var { StyleSheet, Text, View, ListView, TouchableHighlight } = React;

class Games extends React.Component {
constructor(props) {
    super(props);
    var ds = new ListView.DataSource({
        rowHasChanged: (r1, r2) => r1 != r2
    });
    this.state = {
        ds: [
            { AwayTeam: "TeamA", HomeTeam: "TeamB", Selection: "AwayTeam" },
            { AwayTeam: "TeamC", HomeTeam: "TeamD", Selection: "HomeTeam" }
        ],
        dataSource: ds
    };
}

componentDidMount() {
    this.setState({
        dataSource: this.state.dataSource.cloneWithRows(this.state.ds)
    });
}
pressRow(rowData) {
    var newDs = [];
    newDs = this.state.ds;
    newDs[0].Selection = newDs[0] == "AwayTeam" ? "HomeTeam" : "AwayTeam";
    this.setState({
        dataSource: this.state.dataSource.cloneWithRows(newDs)
    });
}

renderRow(rowData) {
    return (
        <TouchableHighlight
            onPress={() => this.pressRow(rowData)}
            underlayColor="#ddd"
        >
            <View style={styles.row}>
                <Text style={{ fontSize: 18 }}>
                    {rowData.AwayTeam} @ {rowData.HomeTeam}{" "}
                </Text>
                <View style={{ flex: 1 }}>
                    <Text style={styles.selectionText}>
                        {rowData[rowData.Selection]}
                    </Text>
                </View>
            </View>
        </TouchableHighlight>
    );
}
render() {
    return (
        <ListView
            dataSource={this.state.dataSource}
            renderRow={this.renderRow.bind(this)}
        />
    );
}
}
var styles = StyleSheet.create({
 row: {
    flex: 1,
    flexDirection: "row",
    padding: 18,
    borderBottomWidth: 1,
    borderColor: "#d7d7d7"
},
selectionText: {
    fontSize: 15,
    paddingTop: 3,
    color: "#b5b5b5",
    textAlign: "right"
}
});


module.exports = Games

The issue I am having comes in the pressRow method. When the user presses the row, I would like the selection to change and for it to render the change on the device. Through some debugging, I have noticed that even though I am changing the Selection property of the object in the newDs array, the same property changes on the object in this.state.ds and similarly changes the object in this.state.dataSource._dataBlob.s1. Through further debugging, I have found that since those other arrays have changed, the ListView's DataSource object doesn't recognize the change because when I set the state and rowHasChanged is called, the array it is cloning matches the array this.state.dataSource._dataBlob.s1 and so it doesn't look like a change and doesn't rerender.

Any ideas?

Brandon M
  • 933
  • 1
  • 8
  • 8

3 Answers3

18

Try this:

pressRow(rowData){

    var newDs = [];
    newDs = this.state.ds.slice();
    newDs[0].Selection = newDs[0] == "AwayTeam" ? "HomeTeam" : "AwayTeam";
    this.setState({
      dataSource: this.state.dataSource.cloneWithRows(newDs)
    })

}

This should make a copy of the array, which can then be modified independently of the original array in this.state.ds.

eddyjs
  • 1,212
  • 10
  • 19
  • Thanks so much! There was one other step I had to do to make it work. For some reason I had to 'clear' the object I was trying to change. So I just made `newDs[0]` equal to a new object and gave it the properties of the object i was trying to change. Of course I changed the `Selection` property though. – Brandon M Jul 31 '15 at 12:57
  • 1
    Change `this.state.ds.slice()` to `this.state.ds.slice(0)` for better performance. – TheNickyYo Feb 24 '17 at 07:25
  • @TheNickyYo - why is that? The docs said undefined is treated as 0 anyway? – Martin Capodici Apr 12 '17 at 06:57
  • `this.state.ds.slice()` is not a function – TomSawyer Jun 06 '17 at 16:04
18

In case anyone comes across this issue like I did, here's the complete solution.

Basically, there seems to be a bug with the ListView component and you need to rebuild each item that changes in the datasource for the ListView to redraw it.

First, create the datasource and a copy of the array and save them to state. In my case, foods is the array.

constructor(props){
    super(props);
    var ds = new ListView.DataSource({
        rowHasChanged: (row1, row2) => row1 !== row2,
    });
    this.state = {
        dataSource: ds.cloneWithRows(foods),
        db: foods,
    };
}

When you want to change something in the datasource, make a copy of the array you saved to the state, rebuild the item with the change and then save the new array with the change back to the state (both db and datasource).

onCollapse(rowID: number) {
    console.log("rowID", rowID);
    var newArray = this.state.db.slice();
    newArray[rowID] = {
        key: newArray[rowID].key,
        details: newArray[rowID].details,
        isCollapsed: newArray[rowID].isCollapsed == false ? true : false,
    };
    this.setState({
        dataSource: this.state.dataSource.cloneWithRows(newArray),
        db: newArray,
    });
}
MatPag
  • 29,651
  • 11
  • 74
  • 87
Dev01
  • 10,909
  • 17
  • 64
  • 109
  • 1
    Instead of manually rebuilding the whole object, you should be able to do: `var copy = Object.assign({}, obj);` per MDN documentation here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign – Eric Goldberg Mar 11 '16 at 21:21
  • 3
    Or if you prefer ES6 syntax: `newRows[rowId] = { ...this._sampleData[rowId], favorited: rowData.favorited ? false : true }` – Eric Goldberg Mar 11 '16 at 21:33
  • May this.db instead of this.state.db be better? – iou90 Nov 11 '16 at 08:18
3

react is smart enough to detect changes in dataSource and if the list should be re-rendered. If you want to update listView, create new objects instead of updating the properties of existing objects. The code would look something like this:

let newArray = this._rows.slice();
newArray[rowID] = {
  ...this._rows[rowID],
  Selection: !this._rows[rowID].Selection,
};
this._rows = newArray;

let newDataSource = this.ds.cloneWithRows(newArray);
this.setState({
  dataSource: newDataSource
});

You can read more about similar issue on Github