Callbacks when Scroll Reaches Top or Bottom of the List
Posted By: Harish

On this blog, we discussed different ScrollView scroll events like start or end drag, start or end momentum etc. These ScrollView events can also be used for list components like VirtualizedList or FlatList.
In this post, we will discuss list component’s callbacks, when scroll reaches top or bottom of the list. To invoke a callback when it reaches top position, we can use onStartReached callback and onEndReached callback when scrolling reaches the bottom of the list.
These callbacks are only for List components like VirtualizedList or FlatList and not for ScrollView. More info will be discussed with an example below.
We will be using the VirtualizedList component as an example. Process is the same for FlatList too.
Create A New Project
Create a new react-native project by using npx. Check documentation for creating a new react native project.
npx react-native@latest init VListRN
Example Implementation
We will create a simple vertical VirtualizedList and add an item to bottom when list reaches bottom or to top of the list when the scrolling reaches start position.
Import and add VirtualizedList
. Add basic VirtualizedList with color blocks. Check basic VirtualizedList implementation for more info.
//App.tsx
...
import { VirtualizedList } from 'react-native';
...
<VirtualizedList
contentContainerStyle={styles.content_container}
data={listData}
getItemCount={getItemCount}
getItem={getItem}
keyExtractor={item => item?.id}
renderItem={({ item }) => {
return (
<Item id={item.id} color={item.color} />
)
}}
/>
...
Complete code can be found at the bottom of this post.
If you run the app,
#for Android
npx react-native run-android
#for ios
npx react-native run-ios
A list of five items can be seen with their id's.
First, we will start with the list when it reaches the bottom.
If we look into the web pagination process, list data will be loaded in parts with the clicks on page numbers. This type of loading increases the speed of the page and saves bandwidth.
Similarly, for mobile android or iOS apps, we can load a few items of the list and later load another set when scroll/list reaches bottom using onEndReached callback.
I will show a basic example with onEndReached callback by adding a new item at the bottom of the list when the scroll reaches the last item.
So, add onEndReached
callback to the VirtualizedList component and console log the event.
...
<VirtualizedList
...
onEndReached={e => {
console.log(e)
}}
/>
...
If you re-run the metro builder and scroll down the list, this callback will get invoked with a distanceFromEnd log.
LOG {"distanceFromEnd": 0}
Here, distanceFromEnd
is nothing but the distance from the bottom of the list when this callback is invoked. Now add a new item to the list whenever this callback is called.
...
<VirtualizedList
...
onEndReached={e => {
const id = listData.length + 1
listData.push({
id: id,
color: 'pink'
});
}}
/>
...
If you scroll down further, new items will get added and we can see that the distanceFromEnd is always 0
, meaning onEndReached
is invoked when the scroll reaches the last item of the list.
But in general, loading new items from an API after reaching the last point of the list may take time. So sometimes we may want to invoke this callback even before reaching the last point.
In those cases, we can use onEndReachedThreshold prop. This prop accepts number values between 0
to 1
. Like 0.1, 0.2 etc. And default is 0.
With this prop, we can tell the list when to invoke the onEndReached callback. We can't specify the exact distance, but use guesswork to use the values. Like for value 0
, it gets called when the list scroll reaches the complete bottom. When the value is 1
, this gets invoked when the list is somewhat far from the bottom. Based on the height of an item and our usage, we have to specify the value. In general, we use the 0.5
value.
...
<VirtualizedList
...
onEndReached={e => {
const id = listData.length + 1
listData.push({
id: id,
color: 'pink'
});
}}
onEndReachedThreshold={0.5}
/>
...

onStartReached callback for top scroll
Similar to onEndReached callback, this onStartReached callback gets called when the user scrolls to the starting point of the list.
Add onStartReached
to VirtualizedList and console log the event.
...
<VirtualizedList
...
onStartReached={(e) => {
console.log(e)
}}
/>
...
If you re-run the metro builder, you will see a log after the render.
LOG {"distanceFromStart": 0}
Here, distanceFromStart
is nothing but the distance from the top point of the list when this callback is invoked.
Now, add a new item to the starting of the list whenever the scroll reaches the starting point.
Before adding an item, we have to add an extra callback, at least for us.
When an item is added to the list on onEndReached callback, the new item adds and goes out of the render window without automatically scrolling to the new item. But for onStartReached callback, the new item is added at the starting point, which is y=0 and pushes the old items downwards.
This invokes repeated callback calls. So to avoid that, at least for this example, I'm using onScrollBeginDrag callback to update a state boolean variable to know when we drag to start scrolling.
And will use that variable to add a new item in onStartReached callback.
...
<VirtualizedList
...
onStartReached={(e) => {
if (!isDragged) return
const id = listData.length + 1
listData.unshift({
id: id,
color: 'bisque'
})
setIsDragged(false)
}}
onStartReachedThreshold={0.5}
onScrollBeginDrag={() => setIsDragged(true)}
/>
...
So, when we start dragging, onScrollBeginDrag invokes and we set the isDragged state variable to true
. And we used this to add a new item at the start of the list as above.
Similar to onEndReachedThreshold prop, onStartReachedThreshold prop is used to set a guessed distance to invoke onStartReached callback.

If you are using these callbacks for infinite scrolling of list data, then a few other props are useful to know for better performance.
Fix the number of items to load on render
This initialNumToRender prop is best used to fix the number items to render on first load. This will be useful when you have many list items.
...
<VirtualizedList
...
initialNumToRender={10}
/>
...
Max number of items to render for next batch of items
If we take this infinite scroll example list as an example with initialNumToRender prop, getting next batch of items may load the remaining items at once. So, to avoid that, we can use maxToRenderPerBatch prop to limit the number of items to render for consecutive renders. Default value is 10. Meaning, 10 items will get rendered for each batch update.
...
<VirtualizedList
...
maxToRenderPerBatch={100}
/>
...
How frequent this next batch of items should render
With updateCellsBatchingPeriod prop, we can fix the frequency of rendering items on demand. Default is 50 in milliseconds.
In simple words, if this value is more, the next render occurs after this time. Less time means fast rendering of items.
...
<VirtualizedList
...
updateCellsBatchingPeriod={200}
/>
...
Check optimizing FlatList page for more info on optimizing your list.
Complete code of our example,
//App.tsx
import React, { useState } from "react";
import {
Text,
StyleSheet,
SafeAreaView,
StatusBar,
View,
VirtualizedList,
} from "react-native";
type ItemDataProp = {
id: number;
color: string;
};
let listData: ItemDataProp[] = [
{
id: 1,
color: 'salmon'
},
{
id: 2,
color: 'salmon'
},
{
id: 3,
color: 'salmon'
},
{
id: 4,
color: 'salmon'
},
{
id: 5,
color: 'salmon'
}
];
export default function App() {
const [isDragged, setIsDragged] = useState(false);
const Item = (props: ItemDataProp) => {
const { id, color } = props;
return (
<View
key={id}
style={[
styles.view,
{
backgroundColor: color
}
]}
>
<Text style={{ color: 'black', fontSize: 30 }}>{id}</Text>
</View>
)
};
const getItem = (data: ItemDataProp[], index: number) => data[index];
const getItemCount = (data: ItemDataProp[]) => data.length;
return (
<SafeAreaView style={{ flex: 1, backgroundColor: 'white' }}>
<StatusBar
barStyle="dark-content"
/>
<View style={styles.container}>
<Text style={styles.text}>
ifelsething.com
</Text>
<Text style={styles.text}>
callbacks when list reaches start or end
</Text>
<VirtualizedList
contentContainerStyle={styles.content_container}
data={listData}
onStartReached={() => {
if (!isDragged) return
const id = listData.length + 1
listData.unshift({
id: id,
color: 'bisque'
})
setIsDragged(false)
}}
onStartReachedThreshold={0.5}
onEndReached={e => {
const id = listData.length + 1
listData.push({
id: id,
color: 'pink'
});
}}
onScrollBeginDrag={() => setIsDragged(true)}
onEndReachedThreshold={0.5}
getItemCount={getItemCount}
getItem={getItem}
keyExtractor={item => item?.id + ''}
renderItem={({ item }) => {
return (
<Item id={item.id} color={item.color} />
)
}}
/>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
margin: 10,
gap: 20
},
text: {
fontSize: 15,
color: 'black',
fontStyle: 'italic'
},
content_container: {
gap: 10
},
view: {
justifyContent: 'center',
alignItems: 'center',
width: '100%',
borderRadius: 10,
height: 200
}
});