React-native FlatList not rerendering row when props change

Viewed 51059

I'm having an issue with the new FlatList component. Specifically, it does not rerender it's rows, even though props that the row is dependent on changes.


The FlatList docs says that:

This is a PureComponent which means that it will not re-render if props remain shallow- equal. Make sure that everything your renderItem function depends on is passed as a prop that is not === after updates, otherwise your UI may not update on changes. This includes the data prop and parent component state.

THE QUESTION

However, seeing as I change an ID of the selectedCategory item - the prop that should indicate whether the row is 'selected' or not - I believe that the props should rerender. Am I mistaken?

I checked the 'componentWillReceiveProps' methods of both the list and row components, and the list receives the update just fine, but the row's lifecycle method is never called.

If I include a random, useless boolean state value in the list component, and switch it back and forth when the props update, it works - but I don't know why?

state = { updated: false };

componentWillReceiveProps(nextProps) {
  this.setState(oldstate => ({
    updated: !oldstate.updated,
  }));
}

<FlatList
  data={this.props.items.allAnimalCategories.edges}
  renderItem={this._renderRow}
  horizontal={true}
  keyExtractor={(item, index) => item.node.id}
  randomUpdateProp={this.state.updated}
/>

THE CODE

The structure of my code is this: I have a container component with all the logic and state, which contains a FlatList component (presentational, no state), which again contains a custom presentational row.

Container
  Custom list component that includes the FlatList component
  (presentational, stateless) and the renderRow method
    Custom row (presentational, stateless)

The container includes this component:

 <CustomList
   items={this.props.viewer}
   onCategoryChosen={this._onCategoryChosen}
   selectedCategory={this.state.report.selectedCategory}
 />

CustomList:

class CustomList extends Component {
  _renderRow = ({ item }) => {
    return (
      <CustomListRow
        item={item.node}
        selectedCategory={this.props.selectedCategory}
        onPressItem={this.props.onCategoryChosen}
      />
    );
  };

  render() {
    return (
      <View style={_styles.container}>
        <FlatList
          data={this.props.items.categories.edges}
          renderItem={this._renderRow}
          horizontal={true}
          keyExtractor={(item, index) => item.node.id}
          randomUpdateProp={this.state.updated}
        />
      </View>
    );
  }

}

(data comes from Relay)

Finally the row:

render() {
    const idsMatch = this.props.selectedCategory.id == this.props.item.id;
    return (
      <TouchableHighlight onPress={this._onItemPressed}>
        <View style={_styles.root}>
          <View style={[
              _styles.container,
              { backgroundColor: this._getBackgroundColor() },
            ]}>
            {idsMatch &&
              <Image
                style={_styles.icon}
                source={require('./../../res/img/asd.png')}
              />}
            {!idsMatch &&
              <Image
                style={_styles.icon}
                source={require('./../../res/img/dsa.png')}
              />}
            <Text style={_styles.text}>
              {capitalizeFirstLetter(this.props.item.name)}
            </Text>
          </View>
          <View style={_styles.bottomView}>
            <View style={_styles.greyLine} />
          </View>
        </View>
      </TouchableHighlight>
    );
  }

The row is not that interesting, but I included it to show that it is entirely stateless and dependent on it's parents props.

The state is updated like so:

_onCategoryChosen = category => {
    var oldReportCopy = this.state.report;
    oldReportCopy.selectedCategory = category;
    this.setState(Object.assign({}, this.state, { report: oldReportCopy }));
  };

State looks like this:

state = {
    ...
    report: defaultStateReport,
  };

const defaultStateReport = {
  selectedCategory: {
    id: 'some-long-od',
    name: '',
  },
  ...
};

9 Answers

Simply you can solve this problem passing props to extraData in flat list component like this,

  <FlatList
    data={this.props.data}
    extraData={this.props}
    keyExtractor={this._keyExtractor}
    renderItem={this._renderItem}
  />

In my case I just made a simple mistake when using keyExtractor

I changed

keyExtractor={(item, index) => index.toString()} 

To

keyExtractor={(item, index) => item.key} 

I was seeing some strange effect after filtering my list where props of filtered out components were being rendered in place of new component's props, my hunch is that because I was using the index of the array rather than a unique key, I was just passing the old props to the new component even though the base component was in fact changing.

I agree with Nimelrian. Also, If your state is an Array you could create an Array Object from the state by doing:

 var oldReportCopy = Object.assign([], this.state.report);

Then use the .push() method to add your new object to it like this:

oldReportCopy.push(selectedCategory);

you can then set this new Array Object back to state:

this.setState({ report: oldReportCopy });

Maybe this won't be the case for anyone else but I realized I was only having trouble when the array of items being rendered by the FlatList became empty. In my case I just needed to not render the FlatList at all, and instead render a different View in its place, and that of course fixed my issue with it "not re-rendering".

Look at line #4

_onCategoryChosen = category => {
    var oldReportCopy = this.state.report;
    oldReportCopy.selectedCategory = category;
    this.setState({ report: [...oldReportCopy] }); // Notice this line
  };

this wasn't working for me

       setTabData(tabD);

and this worked for me

      setTabData([...tabD]);

In react hooks you can do something like this:

const onPressLeaderSelect = (item, index) => {
    let oldMemberCopy = Object.assign([], teamMemberArr);  //'teamMemberArr' is local state
    let objIndex = oldMemberCopy.findIndex((obj => obj.teamLeader == 1));
    oldMemberCopy[objIndex].teamLeader = 0
    oldMemberCopy[index].teamLeader = 1
    console.log('onPressLeaderSelect', oldMemberCopy)
    setteamMemberArr(oldMemberCopy)
}

this might be helpful FlatList code

/**
 * Multiple columns can only be rendered with `horizontal={false}` and will zig-zag like a `flexWrap` layout.
 * Items should all be the same height - masonry layouts are not supported.
 */
numColumns?: number | undefined;
Related