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.
For the main error you mentioned in your question:
- Without
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. - With
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 the useEffect
dependency array to avoid triggering re-renders unnecessarily.
Best Practices:
- Use
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.- Use
keyExtractor
: Always ensure a stable and unique key for each list item to help React optimize rendering efficiently.
Here is the sample solution:
import React, {useRef} from 'react';
import {FlatList} from 'react-native';
const HomeScreen = () => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [displayItems, setDisplayItems] = useState<Item[] | null>(null);
const nextRef = useRef(BASE_URL);
const fetchItems = async () => {
setIsLoading(true);
const response = await client(nextRef.current, 'GET'); // Just calls axios
setDisplayItems((items) => items.concat(response.data));
nextRef.current = response.next;
setIsLoading(false);
};
React.useEffect(() => {
fetchItems();
}, []);
const onEndReached = () => {
if (!isLoading) {
fetchItems()
}
}
const listEmptyComponent = () => {
if (!isLoading && displayItems?.length === 0) {
return (
<Text>Nothing to show</Text>
)
}
}
const renderItem = ({item}) => {
return <ShowItem item={item}/>
}
const listFooterComponent = () => {
if (isLoading && displayItems?.length > 0) {
return <LoadingSpinner/>
}
}
return (
<FlatList
data={displayItems}
onEndReached={onEndReached}
renderItem={renderItem}
ListFooterComponent={listFooterComponent}
ListEmptyComponent={listEmptyComponent}
keyExtractor={(item) => item.id}
/>
)
};
export default HomeScreen;
3
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:
// Get items on first loading screen
React.useEffect(() => {
if(displayItems == null) fetchItems();
}, [fetchItems,displayItems]);
Doing it this way the useEffect is called when fetchItems
or displayItems
changes, but only calls fetchItems
if displayItems
is null (its initial state). I dont think its necessary to wrap fetchItems
with useCallback, but this effect should work whether fetchItems
is wrapped or not
0