Table of content
Introduction
All modern applications and websites nowadays use infinite scroll for many use cases like news feeds and screens that have infinite number of items. In this tutorial, we will see how to implement infinite scroll in React Native with an example by giving an explanation to every aspect in code.
I already have created a project with all the necessary components to make it easier for you to focus on the concept of infinite scroll and follow along with me.
The application has only one simple screen that shows posts within a Flatlist and the user needs to scroll to see other posts. When the user reaches the end, a shimmer loading indicator is shown, and an HTTP request is made to fetch more posts.
Clone the project
The đź”— application source code is available in github. To clone it, run the following command:
git clone git@github.com:Hostname47/react-native-infinite-scroll.git Infinite
After that install the dependencies:
npm install
Cd to the project and start the application:
cd Infinite && npm run start
Now you should be able to start the application and see the infinite scroll working.
Understand Infinite Scroll
Now, I want you to open a file called Home.jsx within src/screens folder which includes everything we need in this tutorial. Follow along with me step by step because the order matters to understand how infinite scroll works.
// imports
const PAGE_SIZE = 8;
const Home = () => {
const [bootstrapped, setBootstrapped] = useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(false);
const [posts, setPosts] = useState([]);
const fetch = async () => {
axios
.get(
`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=${
PAGE_SIZE + 1
}`
)
.then(response => {
const data = response.data;
const items = response.data.slice(0, 8);
setHasMore(data.length > PAGE_SIZE);
if (posts.length === 0) {
setPosts(items);
} else {
setPosts(v => [...v, ...items]);
}
})
.finally(() => {
if (!bootstrapped) {
setBootstrapped(true);
}
});
};
const fetchMore = () => {
if (!hasMore) {
return;
}
setPage(value => value + 1);
};
useEffect(() => {
fetch();
}, [page]);
return (
<View style={{ flex: 1 }}>
{!bootstrapped ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="black" />
</View>
) : posts.length === 0 ? (
<View style={styles.centeredSection}>
<EmptyIcon width={36} height={36} />
<Text style={styles.sectionTitle}>Empty posts list</Text>
<Text style={styles.sectionSubtitle}>Either change you search query or refresh the page</Text>
</View>
) : (
<View style={styles.container}>
<Search />
<FlatList
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.postsContainer}
data={posts}
keyExtractor={post => post.id}
renderItem={({ item: post }) => <Post post={post} />}
ListFooterComponent={hasMore && <Glimmer />}
onEndReached={fetchMore}
onEndReachedThreshold={0.2}
/>
</View>
)}
</View>
);
};
export default Home;
Now let’s try to cover the aspect of this file step by step:
1. Bootsrapping: First fetch
The bootstrapped state variable handle the case where the user access the screen the first time; When this state is false, we display a loading indicator (spinner) to the user to show him a feedback of data is being fetched as in the JSX as following:
{!bootstrapped ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="black" />
</View>
) : (...)}
Once the first fetch is done, we set the bootstrapped state value to true in the finally part of async-then-finally pipeline within the fetch function. We’ll cover the fetch function later; for now, just remember that the user will see a loading indicator the first time they access the screen. When the fetch function is called, we turn this bootstrapped state to false to show the results.
3. Handle emptiness
Within fetch function, If the API responde with an empty body result, we need show the user a feedback or message that the list of posts is empty as demonstrated in JSX:
posts.length === 0 ? (
<View style={styles.centeredSection}>
<EmptyIcon width={36} height={36} />
<Text style={styles.sectionTitle}>Empty posts list</Text>
<Text style={styles.sectionSubtitle}>
Either change you search query or refresh the page
</Text>
</View>
) : (...)
4. Flatlist, Shimmer and events
In case the fetch function returns some results from the API, we store those posts within a Flatlist. The reason we use Flatlist is because it simplifies how we can add the loading indicator (shimmer) at the end of the list and also provides us with the event responsible for detecting when the user scrolls to the end.
<FlatList
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.postsContainer}
data={posts}
keyExtractor={post => post.id}
renderItem={({ item: post }) => {
return <Post post={post} />;
}}
ListFooterComponent={hasMore && <Shimmer />} // 1
onEndReached={fetchMore} // 2
onEndReachedThreshold={0.2} // 3
/>
4.1. Loading shimmer:
Within ListFooterComponent Flatlist attribute, we display the Shimmer component when the hasMore state varaible is true. We will talk about pagination later when we handle scroll event.
This loading indicator helps the user to understand that there is something being fetched and he needs to wait.
4.2. Scroll event
onEndReached event helps us detect when the user scroll to the end, and we can do something at that time. In our case, we need to fetch more posts by calling fetchMore function.
Within the ListFooterComponent Flatlist attribute, we display the Shimmer component when the hasMore state variable is true. We will talk about pagination later when we handle the scroll event.
This loading indicator helps the user to understand that something is being fetched and they need to wait.
4.3. When exactly should you trigger fetch
Sometimes, we don’t need to fetch more posts when the user scrolls to the very end; instead, we want to run fetch when the loading indicator is shown in the viewport, even though the user does not scroll to the very end. As long as a part of the loading indicator is shown to the user, a fetch should run.
onEndReachedThreshold helps us do that by specifying the value of the distance from the end. Read more about onEndReachedThreshold here.
5. Fetch & Pagination
const fetch = async () => {
axios
.get(
`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=${
PAGE_SIZE + 1
}`
)
.then(response => {
const data = response.data;
const items = response.data.slice(0, 8);
setHasMore(data.length > PAGE_SIZE);
if (posts.length === 0) {
setPosts(items);
} else {
setPosts(v => [...v, ...items]);
}
})
.finally(() => {
if (!bootstrapped) {
setBootstrapped(true);
}
});
};
For demonstration purposes, I used a fake API called JSONPlaceholder to implement the infinite scroll feature, and the axios library to make HTTP requests and fetch data.
The fetch function is a simple function that makes a request to the API to get posts and stores them in the posts state variable.
The fetch function is not called directly when the user first accesses the screen or when they scroll to the very end; instead, an effect is responsible for doing that as follows.
useEffect(() => {
fetch();
}, [page]);
This effect runs the first time the user accesses the screen to perform the initial fetch, and then it sets bootstrapped to false to show them the results.
Additionally, when the user scrolls to the very end, a function called fetchMore (used in the onEndReached attribute within Flatlist) upgrades the page state value. When the page state changes, the effect runs again because it has the page variable as its dependency.
How pagination (infinite scroll) works
Normally, when we make a request, we fetch a number of posts per scroll, such as fetching 8 posts as specified in the PAGE_SIZE constant. However, in the code above, you’ll notice that we fetch PAGE_SIZE + 1. Why is that?
The reason why we fetch PAGE_SIZE + 1 is to determine if there are more posts left after the current fetch and to handle pagination.
For example, let’s imagine we have 16 posts, and we want to fetch 8 posts per scroll.
When we make the first fetch, we don’t fetch 8 posts; instead, we try to fetch 9 posts. Then, when the response is returned, we compare the result with PAGE*SIZE (which is in our example 8). If the result is 9 and PAGE_SIZE is 8, that means there are more posts to be fetched. Based on this condition, we set hasMore to true and show the loading indicator again. But we only take the first 8 items from the 9 items we fetched.
When the first fetch is done, the hasMore should be true because we request 9 and we get 9 from the API which satisfy the condition (result > PAGE_SIZE).
In the second fetch, we need to skip the items we already have within posts state, and fetch 9 more posts; In this case skip 8 posts and get 9 more posts. When the request is done, we get only 8 posts from the API, because we only have 16 posts: In this case the condition is not true which means hasMore should be false, and that make sense to hide loading indicator and prevent fetching more again when the user scroll to the very end.
Remember: The 9th element is used only to check if there are more posts waiting to be fetched and to decide whether to show the loading indicator again and run fetching request.
Handle memoization
When we talk about infinite scroll, we talk about countless number of posts which at some point will affect performance of your app, so It is neccessary to handle memoization to keep the list fast and efficient.
I already handle memoization in Post component within src/components/Post.jsx file by wrapping the component by memoization function to get a memoized version of that component:
export default React.memo(Post);
In featch function, when we fetch a new bunch of posts, we append them to the posts state:
if (posts.length === 0) {
setPosts(items);
} else {
setPosts(v => [...v, ...items]);
}
We do that by destructuring the old value and get the old posts, and append the new posts. At this time the posts that are already shown to the user, do not need to rerender again, and the way we tell react to not rerender the old posts and just append the new posts is by using memo function.
Handle the last scroll
When we fetch a bunch of posts and we get only 8 posts or less than 8, means we don’t have more posts to fetch in the next scroll. In this case, we need to set hasMore to false, and hide the loading indicator and stop fetching when the user scroll to the end.
We already handle this within fetch function in one line:
setHasMore(data.length > PAGE_SIZE);
This is responsible to see if the response data is greather than the page size to determin if there are more posts to be fetched or not.
If you have a large list of data, and you don’t handle memoization, you may get some warnings in your console like following:
LOG VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc. {“contentLength”: 8026, “dt”: 2047, “prevDt”: 649}
Conclusion
Many applications and websites use the infinite scroll feature to simplify the pagination process and provide a good user experience to their users. This blog post shows you in detail how to implement infinite scroll in React Native using simple methods to handle pagination and write an efficient solution for this feature.