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!
For the main error you mentioned in your question:
React.useCallback
, on each render (or state update), such as setNext(response.next)
, a new reference of fetchItems
is created. This forces the useEffect
to be triggered again, causing another call to fetchItems
. This creates a loop of infinite API calls and re-renders.React.useCallback
, since you've included next
(a state variable) in the dependency array, and next
is updated within the fetchItems
function, when next
is updated, React.useCallback
will return a new reference to fetchItems
, and thus useEffect
will be triggered again.Simple Solution: Remove fetchItems
from the useEffect
dependency array to avoid triggering re-renders unnecessarily.
Best Practices:
useRef
instead of useState
for next
: 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:
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;