1

I have a FlatList in React Native which is basically a scrolling list that can trigger a function call when it reaches the end of the list, like a lazy loading component. In order for the list to get the next data I am using pagination, and am storing the current page state in useState. The problem is that when this list updates very fast it ends up using stale pagination state. I'm not sure what to do about that. How do I co-ordinate with FlastList or how do I use an appropriate combo of state and functions to avoid stale state?

const [pagination, setPagination] = React.useState<PaginationParams>({
  limit: 5,
  skip: 0,
});

const getData = async () => {
  // If getData gets called quickly by FlatList, it will use old pagination!
  const response: any = await getData(pagination);
  if (response) {
    setData((prevState) => [...prevState, ...response.data]);
    if (
      typeof pagination.limit !== "undefined" &&
      typeof pagination.skip !== "undefined"
    ) {
      if (response.data.count > pagination.skip) {
        const skip = Math.min(
          pagination.skip + pagination.limit,
          result.data.count
        );
        setPagination((prevState) => ({ ...prevState, skip }));
      }
    }
  }
};

<FlatList
  data={data}
  initialNumToRender={5}
  onEndReached={getData}
  onEndReachedThreshold={0.5}
  renderItem={renderItem}
/>
pbuzz007
  • 747
  • 1
  • 5
  • 15

2 Answers2

2

I don't think you should be having problems with stale state after the new list is rendered, because setData is called synchronously with a possible setPagination. After the API request finishes and the component re-renders, the onEndReached prop definitely will have the most up-to-date pagination.

Rather, the problem you describe sounds like onEndReached is being called multiple times before the API response comes back - which you can fix by only having getData start a request if no API call is currently ongoing.

const requestOngoingRef = useRef(false);

const getData = async () => {
  if (requestOngoingRef.current) return;
  requestOngoingRef.current = true;

  const response: any = await getData(pagination);
  requestOngoingRef.current = false;
  // ...

I'd also strongly recommend against using any, since that defeats the whole purpose of using TypeScript - if you type everything properly, you'll be allowing your setup to automatically guard you against many possible logic errors.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
2

The problem appears with React versions before 18 as setData will cause render and setPagination an another one. Pagination info and data will not be in sync after setData -call. One way to solve this would be to put the values into a same state variable:

const [listState, setListState] = React.useState<PaginationParams>({
  data: [],
  pagination: {
    limit: 5,
    skip: 0,
  }
});

const getData = async () => {
  // If getData gets called quickly by FlatList, it will use old pagination!
  const response: any = await getData(pagination);
  if (response) {
    const newState = {
      data: [...listState.data, ...response.data]);
      pagination: // new pagination state to here
    };
    // ...
  }
};

<FlatList
  data={data}
  initialNumToRender={5}
  onEndReached={getData}
  onEndReachedThreshold={0.5}
  renderItem={renderItem}
/>

Another problem with the code is that it will not identify if the fetch is already running. Another fetch call should be blocked until the new data has been retrieved, but this is outside of the scope of your question.

Another recommendation is to use useCallback with event handler to avoid extra rendering. See the explanation in here.

Ville Venäläinen
  • 2,444
  • 1
  • 15
  • 11