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
onEndReachedevent. Setting theinvertedprop totruewill 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.getItemLayoutscrollToIndex 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}
/>;
}
}