Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React-native FlatList not rerendering row when props change

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: '',
  },
  ...
};

like image 792
jhm Avatar asked May 31 '17 07:05

jhm


People also ask

Why doesn't react native flatlist rerender?

React Native FlatList is a PureComponent, and it will not rerender its items if you're making shallow changes to the props. The same applies to the state variable the list-item depends.

Does flatlist need to re-render if it doesn't set a prop?

Without setting this prop, FlatListwould not know it needs to re-render any items because it is a PureComponentand the prop comparison will not show any changes. keyExtractortells the list to use the ids for the react keys instead of the default keyproperty.

How to boost the performance of a flatlist in react?

On functional components, we use React.memo to boost the performance of the FlatList by memoizing the result, according to React.memo documentation: If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result.

What are the features of React Native?

This is documentation for React Native 0.62, which is no longer actively maintained. For up-to-date documentation, see the latest version ( 0.69 ). A performant interface for rendering basic, flat lists, supporting the most handy features: Fully cross-platform. Optional horizontal mode. Configurable viewability callbacks. Header support.


3 Answers

The problem here lies within the fact that

  1. You are mutating an existing slice of state instead of creating a mutated copy

_onCategoryChosen = category => {
    var oldReportCopy = this.state.report; // This does not create a copy!
    oldReportCopy.selectedCategory = category;
    this.setState(Object.assign({}, this.state, { report: oldReportCopy }));
};

This should be

_onCategoryChosen = category => {
    var oldReportCopy = Object.assign({}, this.state.report);
    oldReportCopy.selectedCategory = category;
    // setState handles partial updates just fine, no need to create a copy
    this.setState({ report: oldReportCopy });
};

  1. The props of FlatList remain the same, your _renderRow function may rely on the selectedCategory prop which does change (If not for the first mistake), but the FlatList component does not know about that. To solve this, use the extraData prop.

     <FlatList
       data={this.props.items.categories.edges}
       renderItem={this._renderRow}
       horizontal={true}
       keyExtractor={(item, index) => item.node.id}
       extraData={this.props.selectedCategory}
     />
    
like image 186
Nimelrian Avatar answered Oct 23 '22 16:10

Nimelrian


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}
  />
like image 26
Janaka Pushpakumara Avatar answered Oct 23 '22 18:10

Janaka Pushpakumara


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.

like image 8
Josh Siegl Avatar answered Oct 23 '22 18:10

Josh Siegl