Bobby Battista
Bobby Battista

Reputation: 2075

Infinite list is not re-rendering after getting new items using Apollo + React Native Query

When I hit the end of the list, fetchmore and updateQuery run the query with the new offset and fetch the new items. But the list itself re-renders with the old data and offset.

I've added the code here for review. Summarizing it, there's a SectionList of recent transactions, which is basically a regular FlatList with items grouped by date here. To get the infinite scroll to work, I followed the docs here for pagination using offset. For some reason, the offset is not increasing to match the size of the list each time I try to fetch more - instead, it updates every other time. How can this be? Console.log statements seem to show the list re-rendering BEFORE the new data is returned.

Log from the first time I hit the end of my list:

EndReached 1: offset is:  10
Top of query function
Top of query function
Query got transactions, returning txnList. txns.length:  10
TransactionList Render: transactions.length:  10
EndReached - UpdateQuery fn... offset:  10
EndReached - UpdateQuery fn... Prev length:  10
EndReached - UpdateQuery fn... Fetchmore length:  10
EndReached - UpdateQuery fn... New data length:  20 .  <---this gets returned and should cause a re-render but it doesn't

Log when I scroll to the end of the list a second time:

EndReached: offset is:  10 <-- should be 20
Top of query function
Top of query function
Query got transactions, returning txnList. txns.length:  20 <--seems to be the transactions from the first updateQuery, this should be 10 new ones
TransactionList Render: transactions.length:  20 
EndReached - UpdateQuery fn... offset:  10
EndReached - UpdateQuery fn... Prev length:  20
EndReached - UpdateQuery fn... Fetchmore length:  10
EndReached - UpdateQuery fn... New data length:  20 <--merged, skipping the duplicates 11-20 because we already had them from before 

code

export class TransactionQuery extends React.Component {
  constructor(props) {
    super(props);
  }
  renderEndReached = (fetchMore, offset) => {
      fetchMore({
        variables: {
          limit: this.props.limit,
          offset: offset,
        },
        updateQuery: (prev, { fetchMoreResult }) => {
          if (!fetchMoreResult) {
            return prev;
          }
          copy = prev;
          copy.user.transactions = unionBy(
            prev.user.transactions,
            fetchMoreResult.user.transactions,
            "id"
          ); // this merges old transactions with new, removing any duplicates by matching transaction ID
          return copy;

        }
      });
    }
  };

  render() {
    return (
        <Query
          query={TXN_DATA}
          fetchPolicy="cache-and-network"
          notifyOnNetworkStatusChange
          variables={{ limit: this.props.limit, offset: 0 }}
        >
          {({ data, error, loading, fetchMore }) => {
            if (error) return <Text>ERROR! O NO! {error}</Text>;
            if (loading) {
              return <Text> LOADING </Text>;
            } // this is annoying and causes the list to snap back to the top each time we fetch more items.
            if (data && data.user) {
              const transactions = data.user.transactions;
              if (transactions) {
                return (
                  <TransactionList
                    transactions={transactions}
                    renderEndReached={() => this.renderEndReached(fetchMore=fetchMore, offset=transactions.length)}
                  />
                );
              } else {
                return <Text>No transactions found</Text>;
              }
            } else {
              return <Text>Transactions query returned no data/user</Text>;
            }
          }}
        </Query>
    );
  }
}

const Transaction = ({ name, date, amount, logoUrl }) => (
  <ListItem icon noBorder>
    <Left>
      <Thumbnail small source={{ uri: logoUrl }} />
    </Left>
    <Body>
      <Text>{name}</Text>
    </Body>
    <Right>
      <USD value={amount} />
    </Right>
  </ListItem>
);

class TransactionList extends React.Component {
  constructor(props) {
    super(props);
  }

  renderItem = ({ item, index, section }) => {
    return <Transaction {...item} />;
  };

  renderHeader = ({ section }) => {
    return (
      <ListItem itemDivider style={{ backgroundColor: "white" }}>
        <Moment element={Text} format="dddd, MMMM D">
          {section.title}
        </Moment>
      </ListItem>
    );
  };

  render() {
    const byDate = groupBy(this.props.transactions, "date");
    const dates = Object.keys(byDate)
      .sort()
      .reverse();
    return (
      <SectionList
        sections={dates.map(date => ({ title: date, data: byDate[date] }))}
        renderItem={this.renderItem}
        renderSectionHeader={this.renderHeader}
        keyExtractor={item => item.id}
        onEndReached={this.props.renderEndReached}
      />
    );
  }
}

Upvotes: 0

Views: 769

Answers (1)

Bobby Battista
Bobby Battista

Reputation: 2075

Fix:

It seems Query won't re-render if it doesn't think the merged object (copy) is different enough from the previous query result - maybe it's a shallow compare? I discovered this fix by adding a test message to the object before returning it.

copy = prev;
copy.user.transactions = [...prev.user.transactions, ...fetchMoreResult.user.transactions]; // this change isn't detected. Shallow compare?
copy.message = "Hi there"; // this fixes the issue but is ugly
return copy;

A neater fix is to use lodash cloneDeep

copy = cloneDeep(prev);
copy.user.transactions = [...prev.user.transactions, ...fetchMoreResult.user.transactions];
return copy;

Upvotes: 1

Related Questions