How can you implement a start reached
event for React Native's FlatList
component?
FlatList already provides an onEndReached
event. Setting the inverted
prop to true
will trigger such event when the list reaches the top, but you will now be left without any event firing at the bottom.
I am posting this as an already answered question in the hope that it will be useful for the community. See my answer (possibly, others) below.
As mentioned in the question:
FlatList already provides an
onEndReached
event. Setting theinverted
prop totrue
will trigger such event when the list reaches the top.
If you don't need both top and bottom events, this is the easiest solution to implement.
I have implemented a custom component which provides an onStartReached
event, and functions in a similar fashion as the onEndReached
event. You can find the code below.
If you think this is useful, glad to help :)
But before you copy-paste the code, please read the following:
onEndReached
and onEndReachedThreshold
Please note that the event info
contains a distanceFromStart
field, as opposed to distanceFromEnd
.onScroll
event, and evaluating when a "top reached" condition is met.onScroll
event handler, the scroll event is forwarded to it.scrollEventThrottle
, by default, is set to 60 FPS (1000/60 = 16.66 ms), but you can override it through props.getItemLayout
scrollToIndex
is called for such featurecomponentDidUpdate
trigger that follows after an onStartReached
event, will check for data
prop change.onStartReached
event, no scroll will occur if:
0
, or negative (when the update results in less items than before)onStartReached
does not
result in an immediate data
prop changehorizontal={true}
lists.ScrollView
based component.
I did not try this. Detecting the "top reached" condition should work the same. To keep the previous scroll position in-place (similar to point 5 above) could be done through scrollToOffset
.RefreshControl
and pull-to-refresh functionalityimport React from "react";
import { FlatList } from "react-native";
// Typing without TypeScript
const LAYOUT_EVENT = {
nativeEvent: {
layout: { width: 0, height: 0, x: 0, y: 0 },
},
target: 0
};
const SCROLL_EVENT = {
nativeEvent: {
contentInset: { bottom: 0, left: 0, right: 0, top: 0 },
contentOffset: { x: 0, y: 0 },
contentSize: { height: 0, width: 0 },
layoutMeasurement: { height: 0, width: 0 },
zoomScale: 1
}
};
// onStartReached
const START_REACHED_EVENT = { distanceFromStart: 0 };
const SCROLL_DIRECTION = {
NONE: 0,
TOP: -1,
BOTTOM: 1
};
export default class BidirectionalFlatList extends React.PureComponent {
constructor(props) {
super(props);
this.ref = this.props.__ref || React.createRef();
this.onLayout = this.onLayout.bind(this);
this.onScroll = this.onScroll.bind(this);
this.onResponderEnd = this.onResponderEnd.bind(this);
this.onStartReached = this.onStartReached.bind(this);
this.previousDistanceFromStart = 0;
this.allowMoreEvents = true;
this.shouldScrollAfterOnStartReached = false;
if (typeof props.getItemLayout !== "function") {
console.warn("BidirectionalFlatList: getItemLayout was not specified. The list will not be able to scroll to the previously visible item at the top.");
}
}
componentDidUpdate(prevProps, prevState) {
const { data } = this.props;
if ((data !== prevProps.data) && (this.shouldScrollAfterOnStartReached === true)) {
const indexToScrollTo = data.length - prevProps.data.length;
if (indexToScrollTo > 0) {
this.ref.current?.scrollToIndex({
animated: false,
index: indexToScrollTo,
viewPosition: 0.0,
viewOffset: 0
});
}
}
this.shouldScrollAfterOnStartReached = false;
}
onStartReached(info = START_REACHED_EVENT) {
if (typeof this.props.onStartReached === "function") {
this.allowMoreEvents = false;
this.shouldScrollAfterOnStartReached = true;
this.props.onStartReached(info);
}
}
onScroll(scrollEvent = SCROLL_EVENT) {
if (typeof this.props.onScroll === "function") {
this.props.onScroll(scrollEvent);
}
// Prevent evaluating this event when the list is horizontal
if (this.props.horizontal === true) { return; }
const { nativeEvent: { contentOffset: { y: distanceFromStart } } } = scrollEvent;
const hasReachedScrollThreshold = (distanceFromStart <= this.scrollThresholdToReach);
const scrollDirection = ((distanceFromStart - this.previousDistanceFromStart) < 0)
? SCROLL_DIRECTION.TOP
: SCROLL_DIRECTION.BOTTOM;
this.previousDistanceFromStart = distanceFromStart;
if (
(this.allowMoreEvents === true) &&
(hasReachedScrollThreshold === true) &&
(scrollDirection === SCROLL_DIRECTION.TOP)
) {
this.onStartReached({ distanceFromStart });
}
}
onResponderEnd() {
this.allowMoreEvents = true;
if (typeof this.props.onResponderEnd === "function") {
this.props.onResponderEnd();
}
}
onLayout(layoutEvent = LAYOUT_EVENT) {
const { onStartReachedThreshold = 0.0, onLayout } = this.props;
if (typeof onLayout === "function") {
onLayout(layoutEvent);
}
this.scrollThresholdToReach = layoutEvent.nativeEvent.layout.height * onStartReachedThreshold;
}
render() {
const {
__ref = this.ref,
onLayout = (event = LAYOUT_EVENT) => { },
onStartReached = (event = START_REACHED_EVENT) => { },
onStartReachedThreshold = 0.0,
scrollEventThrottle = 1000 / 60,
...FlatListProps
} = this.props;
return <FlatList
ref={__ref}
{...FlatListProps}
onLayout={this.onLayout}
onScroll={this.onScroll}
scrollEventThrottle={scrollEventThrottle}
onResponderEnd={this.onResponderEnd}
/>;
}
}
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import BidirectionalFlatList from "./BidirectionalFlatList";
const COUNT = 10;
const ITEM_LENGTH = 40;
const styles = StyleSheet.create({
list: { flex: 1 },
listContentContainer: { flexGrow: 1 },
item: {
flexDirection: "row",
alignItems: "center",
width: "100%",
height: ITEM_LENGTH
}
});
function getItemLayout(data = [], index = 0) {
return { length: ITEM_LENGTH, offset: ITEM_LENGTH * index, index };
}
function keyExtractor(item = 0, index = 0) {
return `year_${item}`;
}
function Item({ item = 0, index = 0, separators }) {
return <View style={styles.item}>
<Text>{item}</Text>
</View>;
}
class BidirectionalFlatListExample extends React.PureComponent {
constructor(props) {
super(props);
this.count = COUNT;
this.endYear = (new Date()).getFullYear();
this.canLoadMoreYears = true;
this.onStartReached = this.onStartReached.bind(this);
this.onEndReached = this.onEndReached.bind(this);
this.updateYearsList = this.updateYearsList.bind(this);
const years = (new Array(this.count).fill(0))
.map((item, index) => (this.endYear - index))
.reverse();
this.state = { years };
}
onStartReached({ distanceFromStart = 0 }) {
if (this.canLoadMoreYears === false) { return; }
this.count += COUNT;
this.updateYearsList();
}
onEndReached({ distanceFromEnd = 0 }) {
this.endYear += COUNT;
this.count += COUNT;
this.updateYearsList();
}
updateYearsList() {
this.canLoadMoreYears = false;
const years = (new Array(this.count).fill(0))
.map((item, index) => (this.endYear - index))
.reverse();
this.setState({ years }, () => {
setTimeout(() => { this.canLoadMoreYears = true; }, 500);
});
}
render() {
return <BidirectionalFlatList
style={styles.list}
contentContainerStyle={styles.listContentContainer}
data={this.state.years}
renderItem={Item}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
onStartReached={this.onStartReached}
onStartReachedThreshold={0.2}
onEndReached={this.onEndReached}
onEndReachedThreshold={0.2}
/>;
}
}