I’ve seen some solutions for this online, but I can’t get any of them to work.
I have a React Native app which loads data from an API. The data is paginated; each time I retrieve a page, I receive the results for that page, plus the url for the next page. So a typical response from the API is in this format (obviously it’s a bit more complex than this, but this is the gist):
{
data: [
{ key: xx, title: 'Item 1' },
{ key: yy, title: 'Item 2' }
],
next: 'www/url/to/next/page/of/results'
}
I want to display each item on screen, and when the user scrolls to the bottom of the screen, the next set of results should load. I’m trying to use a FlatList
for this.
So far I have (I don’t have any kind of error checking or anything in yet; just trying to get it to work first):
const HomeScreen = () => {
const [next, setNext] = React.useState<string>(BASE_URL); // URL of first page of results
const [isLoading, setIsLoading] = React.useState<boolean>(true);
const [displayItems, setDisplayItems] = React.useState<Item[]|null>(null);
// Get next page of items
const fetchItems = async () => {
setIsLoading(true);
const response = await client(next, 'GET'); // Just calls axios
setDisplayItems((items) => items.concat(response.data));
setNext(response.next);
setIsLoading(false);
};
// Get items on first loading screen
React.useEffect(() => {
fetchItems();
}, [fetchItems]);
// Show items
if (isLoading) return <LoadingSpinner />
if (displayItems && displayItems.length === 0) return <Text>Nothing to show</Text>
return <FlatList
onEndReachedThreshold={0}
onEndReached={fetchItems}
data={displayItems}
renderItem={(i) => <ShowItem item={i}/>} />
};
export default HomeScreen;
The problem with this is that it flags an error saying The 'fetchItems' function makes the dependencies of useEffect Hook change on every render.
. It suggests To fix this, wrap the definition of 'fetchItems' in its own useCallback() Hook.
.
So I wrap it in a useCallback()
hook:
const fetchItems = React.useCallback(async () => {
setIsLoading(true);
const response = await client(next, 'GET'); // Just calls axios
setDisplayItems((items) => items.concat(response.data));
setNext(response.next);
setIsLoading(false);
}, [next]);
This doesn’t run at all unless I add fetchItems()
, but at that point it re-renders infinitely.
I can’t find anything online that has worked. The annoying thing is that I remember implementing this for another project a few years ago, and I don’t remember it being particularly complicated! Any help appreciated.
2
Answers
The comment on your useEffect suggest that the purpose of that effect is to fetch data on the initial render; so you need to add a conditional to that effect to ensure it does that:
Doing it this way the useEffect is called when
fetchItems
ordisplayItems
changes, but only callsfetchItems
ifdisplayItems
is null (its initial state). I dont think its necessary to wrapfetchItems
with useCallback, but this effect should work whetherfetchItems
is wrapped or notFor the main error you mentioned in your question:
React.useCallback
, on each render (or state update), such assetNext(response.next)
, a new reference offetchItems
is created. This forces theuseEffect
to be triggered again, causing another call tofetchItems
. This creates a loop of infinite API calls and re-renders.React.useCallback
, since you’ve includednext
(a state variable) in the dependency array, andnext
is updated within thefetchItems
function, whennext
is updated,React.useCallback
will return a new reference tofetchItems
, and thususeEffect
will be triggered again.Simple Solution: Remove
fetchItems
from theuseEffect
dependency array to avoid triggering re-renders unnecessarily.Best Practices:
useRef
instead ofuseState
fornext
: This prevents unnecessary re-renders, as useRef updates without triggering a re-render, unlike useState.onEndReached
should only be called if there is no API call (data fetching) in progress: Implement a flag(isLoading
) or condition to ensure you don’t trigger additional fetches while one is already happening.keyExtractor
: Always ensure a stable and unique key for each list item to help React optimize rendering efficiently.Here is the sample solution: