11

Is it possible to force a ListView to re-render, even if the data in the dataSource has not changed? I have a ListView within a tab bar in my app and I want it to redraw every time that tab is selected, regardless of if the data is the same or has changed.

this.state = {
  data: props.data,
  dataSource: new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2})
}

componentWillMount() {
  this.setState({
    dataSource: this.state.dataSource.cloneWithRows(nextProps.data)
  })
}

render() {
  <ListView
   dataSource={this.state.data}
   renderRow={this._renderRow}
  />
}

I tried playing with the rowHasChanged arguments but that did not help. Any help would be much appreciated

Brien Crean
  • 2,599
  • 5
  • 21
  • 46
  • Maybe add on an `onClick` function to the tab and when that function is called make it call the render function? – cameck Jun 23 '16 at 19:58

5 Answers5

35

So your use-case, as I understand it, is to re-render all rows of ListView because of value changes. I don't know what value is, up to you to determine it, but I will use value to explain my answer:

There are a few ways to solve this. each have pros and cons:

  • easy way: just <ListView key={value} ... /> !! It tells React that ListView needs to get re-render when value changes (it will be unmounted, remounted).
  • "hacky way". replace with a new DataSource. this.setState({ dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }).cloneWithData(this.props.data) });
  • the (imo) best practice way: in the data, incorporate value and implements rowHasChanged properly. For instance: (r1, r2) => r1.value !== r2.value && r1.data !== r2.data (<- this is JUST a possible way to represent the data, you have to chose your format, I just assumed you did clone dataSource with nextProps.data.map(data => ({ data, value })).

to me, the third and last solution is the best because:

  • it scales nicely to more condition of re-rendering (other values)
  • it can happen that only part of your rows actually should get re-rendered, and, in that case, you shouldn't re-render all rows for better performance.

here is an other answer I gave on that subject:

What are the exact inputs to rowHasChanged in ListView.DataSource

Community
  • 1
  • 1
gre
  • 1,841
  • 2
  • 16
  • 26
  • Thank you for your answer, my mind is blown. I had no idea r1 and r2 meant old and new row. Makes so much more sense now. Wish they could have referenced that better! – Brien Crean Jun 25 '16 at 05:04
  • 1
    There are some cases where the "hacky way" seems to be the only way to do what you need. For instance, I'm working on a case where a model in the list of models displayed by the ListView is modified somewhere else in the app. In order to show this change, we have to modify the entire DataSource, because the ListView will just do nothing if you don't. It's a developer opinion from React that an entire list should be considered as immutable as the objects inside that list. – mienaikoe Jan 17 '17 at 01:11
  • 1
    @mienaikoe implementing `rowHasChanged` is actually the opportunity to choose if you consider your list object immutable or not. It's really up to you to implement it and only the default suggested `r1!==r2` is a "developer opinion from React" in favor of immutability objects but the ListView itself is not that opinion (instead of the experimental WindowedListView for instance). `rowHasChanged` gives you control of the equality function and immutability or not of your data. BTW this whole thing is a performance tradeoff and maybe we'll find something better in the future. – gre Mar 01 '17 at 09:30
  • like @mienaikoe says the hacky way helps at time. and also to note the answer has `cloneWithData` which didn't work for me so had to change to `cloneWithRow` thanks @gre – Ismail Iqbal May 03 '17 at 06:41
  • I always thought `r1` and `r2` were different rows akin to `sort((a, b) => ...`. Now that I realize it's `prevRow` vs. `nextRow`, the `rowHasChanged` function finally makes sense to me! – Ryan H. May 08 '17 at 16:23
  • The "hacky" way re-renders the entire ListView and resets its scroll position to zero. Since I was using a horizontal list view for paging, I did not want to do it this way. I still managed to keep my data coming from Redux "immutable" but added the additional data I needed for my `rowHasChanged` logic by creating a new data object that included both the Redux data and the additional data just before calling `cloneWithRows` -- though this may not be efficient for large data sets. – Ryan H. May 08 '17 at 16:23
2

Use an Immutability Approach

The best way to "update" this would be to clone the data item in question rather than doing a flag hack as the r1 !== r2 is already hinting at. Instead of updating an individual item, change the "pointer" itself by cloning.

For this particular issue update the item in the array like so.

arr[i] = { ...arr[i], someKey: newValue }

This will replace the "someKey" property with "newValue" and the current row will have a new pointer.

Here is a good resource to learn about immutability in modern JavaScript that react native uses: https://wecodetheweb.com/2016/02/12/immutable-javascript-using-es6-and-beyond/

King Friday
  • 25,132
  • 12
  • 90
  • 84
0

You can call this.forceUpdate() to force a re-render.

You can do this in a simple onClick handler that you attach to the tab.

More info here, on the official docs

However, if there are other rules dictating if there should be an update or not, you can try replacing the dataSource altogether with a new one, making it have a new reference. That will also make it update.

Something along the lines of

this.setState({ data : newDataSource… });

Elod Szopos
  • 3,475
  • 21
  • 32
0

This is probably a bit overkill but I'm just replacing the datasource on componentDidUpdate (maybe this is bad for performance? I dunno haha)

  componentWillMount() {
   this.ds = new ListView.DataSource({
     rowHasChanged: (r1, r2) => r1 !== r2,
    });
   this.dataSource = this.ds.cloneWithRows(this.props.shiftsList);
  }

componentDidUpdate() {
  this.ds = new ListView.DataSource({
   rowHasChanged: (r1, r2) => r1 !== r2,
  });
 this.dataSource = this.ds.cloneWithRows(this.props.shiftsList);
}
Sam Matthews
  • 677
  • 1
  • 12
  • 27
0

I know I'm a bit late but I'm sure that this is still relevant. There's an even easier approach to this... The first example provided by Jason is by all means a good solution but I'm thinking of those whom easily gets confused. If so, use this.

// On component initalization
componentWillMount() {
    this.UpdateData();
}

// When the component updates
componentDidUpdate() {
    if (this.state.data != this.props.data)
        this.UpdateData();
}

// Function to update the ListView component data
UpdateData() {
    var source = new ListView.DataSource({
        rowHasChanged: (r1, r2) => r1 !== r2
    });

    this.setState({
        data: this.props.data,
        dataSource: source.cloneWithRows(this.props.data)
    });
}

I also recommend to implement Redux in your application. That way you could store the new, updated array value in a reducer and then store it locally in a state and later use an if statement in the componentDidUpdate function to determine whether the data is updated or not.

The React debugger logs no performance based complaints or warnings whatsoever...