React Native: contentOffset y value from onScroll nativeEvent inaccurate - react-native

I have a with the following structure:
const [headerHeight, setHeaderHeight] = useState(undefined)
...
<ScrollView onScroll={({nativeEvent}) => {
console.log(nativeEvent.contentOffset.y)
}}>
<HeaderComponent
onLayout={({nativeEvent}) => {
setHeaderHeight(nativeEvent.layout.height)
}}
/>
<OtherComponent1 />
<OtherComponent2 />
<OtherComponent3 />
</ScrollView>
After everything has rendered, headerHeight would become 300. If I start scrolling, the console.log function from the onScroll prop in the ScrollView would log at around a 150+ and the would be gone from the screen.
Am I missing something? Since the height of the header is at 300 (based on the onLayout function), shouldn't be the nativeEvent.contentOffset.y value reach 300 first before the is gone from the screen? Why is it already gone at around 150+?

Related

React Native SectionList scrollToLocation not working inside a scrollview

I'm trying to have a normal info view and a sectionList inside a scrollView. Though the view gets scrolled, when I try to use scrollToLocation for scrolling to specific index selected it is not scrolling. Also I have tried using other props onMomentumScrollEnd in sectionList which is also not working. When I remove scrollView it works perfectly.
<ScrollView>
<View style={{ height: 300, backgroundColor: 'rgba(0,0,0,0.2)' }} />
<View>
<SectionList
ref={(ref) => (this.contentRef = ref)}
stickySectionHeadersEnabled={false}
showsVerticalScrollIndicator={false}
sections={sectionListData}
keyExtractor={(item) => item.id}
onMomentumScrollEnd={() => {
this.setState({ onScrollFinished: true });
this.setViewableItem();
}}
onScrollEndDrag={() => {
this.setViewableItem();
}}
onViewableItemsChanged={this.onViewableItemsChanged}
renderItem={this.renderSectionItem}
renderSectionHeader={!this.props.isMenuLoading && this.renderSectionHeader}
initialNumToRender={500}
onScrollToIndexFailed={(info) => console.log('info', info)}
/>
</View>
</ScrollView>
setActiveIndex(key) {
this.setState({ activeIndex: key, updatedAt: Date.now() });
if (isValidElement(this.headerRef)) {
this.headerRef.scrollToIndex({ index: key, animated: true, viewPosition: 0.5 });
}
if (isValidElement(this.contentRef)) {
this.contentRef.scrollToLocation({
sectionIndex: key,
itemIndex: 0,
animated: false,
viewPosition: 0
});
}
}
It appears this is a long-standing issue which no one seems to have an answer for. Issue #25295 on the react-native repository from June 2019 reported this behavior and was automatically closed as stale after 3 months. Issue #31136 reports the same problem, and there are presumably other references to it as well.
The behavior appears to be related to the underlying implementation of the scroll* imperative functions. Specifically, the call stack looks something like this:
scrollToIndex in VirtualizedList
scrollToLocation in VirtualizedSectionList
scrollToLocation in SectionList
The bit that fails is when VirtualizedList attempts to call this._scrollRef.scrollTo, which is apparently not defined when the SectionList is nested in a ScrollView or other scrollable component like FlatList.
My suggestion would be to refactor the layout such that there are not scrollable items nested under other scrollable items on the same axis.

onEndReached not working in react-native flatList

<View style={{ flex: 1 }}>
<FlatList style={styles.container}
refreshing={this.state.refreshing}
data={this.props.recieveLetter}
renderItem={this.renderItem}
keyExtractor={extractKey}
ListFooterComponent={this.renderFooter}
onRefresh={this.handleRefresh}
onEndReached={this.onEndReached}
onEndReachedThreshold={0}
/>
</View>
onEndReached is not called when I scrolling to the end and I can't get more data from the API.
Working for me, add below line to FlatList:
onEndReached={this.handleLoadMore.bind(this)} //bind is important
onEndReachedThreshold needs to be a number between 0 and 1 to work correctly, so try setting it to 0.1 if you need a small threshold, or even 0.01, and it should work in most cases.
However, from my testing in react native v0.57.4, onEndReached has an erratic behavior even then, sometimes it's not called when you scroll too quickly in Android, and if you are on iOS and the list does the bounce effect when reaching the end, it may be called several times. The most consistent way of triggering my end of list function was to make an end of list check myself, made possible using the props from ScrollView (which FlatLists accept). I did it using onScroll prop like this:
//Outside of the component
const isCloseToBottom = ({layoutMeasurement, contentOffset, contentSize}) => {
const paddingToBottom = 90; //Distance from the bottom you want it to trigger.
return layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom;
};
//later, In my screen render
<FlatList
//other flatlist props, then...
onScroll={({nativeEvent}) => {
if (isCloseToBottom(nativeEvent)) {
if (!this.state.gettingMoreList) {
this.setState({
gettingMoreList: true
}, () => {
this.loadMoreList(); //Set gettingMoreList false after finishing.
});
}
}
}}
scrollEventThrottle={1000}
/>

React native flatlist initial scroll to bottom

I am trying to create a chat in React native using a <Flatlist />
Like WhatsApp and other chat apps, the messages start at the bottom.
After fetching the messages from my API, I call
this.myFlatList.scrollToEnd({animated: false});
But it scrolls somewhere in the middle and sometimes with fewer items to the bottom and sometimes it does nothing.
How can I scroll initially to the bottom?
My chat messages have different heights, so I can't calculate the height.
I had similar issue. If you want to have you chat messages start at the bottom, you could set "inverted" to true and display your messages and time tag in an opposite direction.
Check here for "inverted" property for FlatList. https://facebook.github.io/react-native/docs/flatlist#inverted
If you want to have you chat messages start at the top, which is what I am trying to achieve. I could not find a solution in FlatList, because as you said, the heights are different, I could not use getItemLayout which make "scrollToEnd" behave in a strange way.
I follow the approach that #My Mai mentioned, using ScrollView instead and do scrollToEnd({animated: false}) in a setTimeout function. Besides, I added a state to hide the content until scrollToEnd is done, so user would not be seeing any scrolling.
I solved this issue with inverted property and reverse function
https://facebook.github.io/react-native/docs/flatlist#inverted
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse
<FlatList
inverted
data={[...data].reverse()}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
You can use this solution in chat component.
I faced the same issue with you and then I moved to use ScrollView.
It is fixed:
componentDidMount() {
setTimeout(() => {
this.scrollView.scrollToEnd();
});
}
<ScrollView ref={(ref) => { this.scrollView = ref; }} style={styles.messages}>
{
messages.map((item, i) => (
<Message
key={i}
direction={item.userType === 'banker' ? 'right' : 'left'}
text={item.message}
name={item.name}
time={item.createdAt}
/>
))
}
</ScrollView>`
Set initialScrollIndex to your data set's length - 1.
I.e.
<Flatlist
data={dataSet}
initialScrollIndex={dataSet.length - 1}
/>
There are two types of 'good' solutions as of 2021.
First one is with timeout, references and useEffect. Here's the full example using Functional Components and Typescript:
// Set the height of every item of the list, to improve perfomance and later use in the getItemLayout
const ITEM_HEIGHT = 100;
// Data that will be displayed in the FlatList
const [data, setData] = React.useState<DataType>();
// The variable that will hold the reference of the FlatList
const flatListRef = React.useRef<FlatList>(null);
// The effect that will always run whenever there's a change to the data
React.useLayoutEffect(() => {
const timeout = setTimeout(() => {
if (flatListRef.current && data && data.length > 0) {
flatListRef.current.scrollToEnd({ animated: true });
}
}, 1000);
return () => {
clearTimeout(timeout);
};
}, [data]);
// Your FlatList component that will receive ref, data and other properties as needed, you also have to use getItemLayout
<FlatList
data={data}
ref={flatListRef}
getItemLayout={(data, index) => {
return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index };
}}
{ ...otherProperties }
/>
With the example above you can have a fluid and animated scroll to bottom. Recommended for when you receive a new message and has to scroll to the bottom, for example.
Apart from this, the second and easier way is by implementing the initialScrollIndex property that will instantly loads the list at the bottom, like that chat apps you mentioned. It will work fine when opening the chat screen for the first time.
Like this:
// No need to use useEffect, timeout and references...
// Just use getItemLayout and initialScrollIndex.
// Set the height of every item of the list, to improve perfomance and later use in the getItemLayout
const ITEM_HEIGHT = 100;
<FlatList
data={data}
getItemLayout={(data, index) => {
return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index };
}}
{ ...otherProperties }
/>
I found a solution that worked for me 100%
Added the ref flatListRef to my flatlist:
<Flatlist
reference={(ref) => this.flatListRef = ref}
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
/>
Then whenever you want to automatically scroll to bottom of the list use:
this.flatListRef._listRef._scrollRef.scrollToEnd({ animating: true });
yes you should access the element _listRef then _scrollRef then call the scrollToEnd 🙄
react-native 0.64.1
react 17.0.2
I've struggled on this as well and found the best possible solution for me that renders without a glitch is:
Use inverted={-1} props
Reverse the order of messages objects inside my array with data={MyArrayofMessages.reverse()} in my case data={this.state.messages.reverse()} using reverse() javascript function.
Stupidly easy and renders instantaneously !
Use inverted={1} and reverse your data by using the JS reverse function. It worked for me
<FlatList contentContainerStyle={{ flex: 1, justifyContent: 'flex-end' }} />
I am guessing that RN cannot guess your layout so it cannot know how much it needs to "move". According to the scroll methods in the docs you might need to implement a getItemLayout function, so RN can tell how much it needs to scroll.
https://facebook.github.io/react-native/docs/flatlist.html#scrolltoend
Guys if you want FlatList scroll to bottom at initial render. Just added inverted={-1} to your FlatList. I have struggle with scroll to bottom for couple of hours but it ends up with inverted={-1}. Don't need to think to much about measure the height of FlatList items dynamically using getItemLayout and initialScrollIndex or whats so ever.
I found a solution that worked for me 100%
let scrollRef = React.useRef(null)
and
<FlatList
ref={(it) => (scrollRef.current = it)}
onContentSizeChange={() =>
scrollRef.current?.scrollToEnd({animated: false})
}
data={data}/>
If you want to display the message inverted, set "inverted" to true in the flat list.
<Flatlist
data={messageData}
inverted={true}
horizontal={false}
/>
If you just want to scroll to the last message, you can use initialScrollIndex
<Flatlist
data={messageData}
initialScrollIndex={messageArray.length - 1}
horizontal={false}
/>
I spent couple of hours struggling with showing the first message on top without being able to calculate the item's height as it contains links and messages. But finally i've been able to...
What i've done is that i wrapped the FlatList in a View, set FlatList as inverted, made it to take all available space and then justified content. So now, conversations with few messages starts at top but when there are multiple messages, they will end on bottom. Something like this:
<View style={ConversationStyle.container}>
<FlatList
data={conversations}
initialNumToRender={10}
renderItem={({ item }) => (
<SmsConversationItem
item={item}
onDelete={onDelete}
/>
)}
keyExtractor={(item) => item.id}
getItemCount={getItemCount}
getItem={getItem}
contentContainerStyle={ConversationStyle.virtualizedListContainer}
inverted // This will make items in reversed order but will make all of them start from bottom
/>
</View>
And my style looks like this:
const ConversationStyle = StyleSheet.create({
container: {
flex: 1
},
virtualizedListContainer: {
flexGrow: 1,
justifyContent: 'flex-end'
}
};

FlatList calls `onEndReached` when it's rendered

Here is render() function for my simple category list page.
Recently I added pagination for my FlatList View so when the user scrolls to the bottom, onEndReached is called in a certain point(onEndReachedThreshold value length from the bottom), and it will fetch the next categories and concatenate the categories props.
But my problem is onEndReached is called when render() is called In other words, FlatList's onEndReached is triggered before it reach the bottom.
Am I putting wrong value for onEndReachedThreshold? Do you see any problem?
return (
<View style={{ flex:1 }}>
<FlatList
data={this.props.categories}
renderItem={this._renderItem}
keyExtractor={this._keyExtractor}
numColumns={2}
style={{flex: 1, flexDirection: 'row'}}
contentContainerStyle={{justifyContent: 'center'}}
refreshControl={
<RefreshControl
refreshing = {this.state.refreshing}
onRefresh = {()=>this._onRefresh()}
/>
}
// curent value for debug is 0.5
onEndReachedThreshold={0.5} // Tried 0, 0.01, 0.1, 0.7, 50, 100, 700
onEndReached = {({distanceFromEnd})=>{ // problem
console.log(distanceFromEnd) // 607, 878
console.log('reached'); // once, and if I scroll about 14% of the screen,
//it prints reached AGAIN.
this._onEndReachedThreshold()
}}
/>
</View>
)
UPDATE I fetch this.props.categories data here
componentWillMount() {
if(this.props.token) {
this.props.loadCategoryAll(this.props.token);
}
}
Try to implement onMomentumScrollBegin on FlatList :
constructor(props) {
super(props);
this.onEndReachedCalledDuringMomentum = true;
}
...
<FlatList
...
onEndReached={this.onEndReached.bind(this)}
onEndReachedThreshold={0.5}
onMomentumScrollBegin={() => { this.onEndReachedCalledDuringMomentum = false; }}
/>
and modify your onEndReached
onEndReached = ({ distanceFromEnd }) => {
if(!this.onEndReachedCalledDuringMomentum){
this.fetchData();
this.onEndReachedCalledDuringMomentum = true;
}
}
I've got it working with
<Flatlist
...
onEndReached={({ distanceFromEnd }) => {
if (distanceFromEnd < 0) return;
...
}
...
/>
First check if the FlatList is inside a ScrollView or Content of native-base. Then take it outside of it
Actually you don't need to use Content or ScrollView, as FlatList has both ListFooterComponent and ListHeaderComponent.
Though it is not recommended, if you really need to use Flatlist inside ScrollView, then take a look at this answer: https://stackoverflow.com/a/57603742/6170191
After hours of trying different approaches I got it to work by wrapping the Flatlist with a View of fixed height and flex:1.
With this settings, I was able to get onEndReached called once and only after I scroll near the bottom. Here's my code sample:
render() {
const {height} = Dimensions.get('window');
return (
<View style={{flex:1, height:height}}>
<FlatList
data={this.props.trips_uniques}
refreshing={this.props.tripsLoading}
onRefresh={()=> this.props.getTripsWatcher()}
onEndReached={()=>this.props.getMoreTripsWatcher()}
onEndReachedThreshold={0.5}
renderItem={({item}) => (
<View style={Style.card}>
...
...
</View>
)}
keyExtractor={item => item.trip_id}
/>
</View>
)
}
My onEndReached() function just calls the API and updates my data. It doesn't do any calculations with regards to distance to bottom or threshold
Most of the times, this error is caused because of an incorrect use of onEndReachedThreashold, which also depends of the number of items you are rendering (more items, more scroll size).
Try to follow this logic:
If 10 items cover your screen, and you are rendering 20 items on each scroll, then set onEndReachedThreashold to 0.8.
If 2 or 3 items cover your screen, and you are rendering 10 items on each scroll, then set onEndReachedThreashold to 0.5.
Also, use initialNumToRender = numItems. For some reason, using this FlatList prop helps to reduce the chance of multiple onEndReached calls.
Just play with onEndReachedThreashold value.
Other times, this error is produced because of nesting scroll views. Do not put your FlatList inside of a ScrollView. Instead, take use of the FlatList header and footer props.
For both solutions, I suggest to set the FlatList style and contentContainerStyle to { flexGrow: 1 }.
Remove every Scrollable View inside your FlatList
If you want to show 3 or 4 records and want to load the next data just when you reach the end. Set onEndReachedThreshold to 0 or 0.1.
Maybe You can bypass this FlatList bug by incrementing your page before doing async call, and then you will fetch data on every onEndReached fiers and not get errors about duplicate keys
(as of NOV19)
Keep flatlist as the only component inside of a single view
Set style of that single view from dimensions like
{{flex: 1, height: Dimensions.get('window').height}}
If FlatList is on another FlatList or ScrollView the onEndReached call immediately when rendered component to resolve that problem doesn't wrap FlatList with another.
A bit late but I just ran into this issue and I fixed it by passing to my <FlatList/> the initialNumToRender prop. This prop is 10 by default so if you don't set it and your screen shows more than 10 items on the initial render, it is going to trigger onEndReached since it has passed the 10th element.
initialNumToRender should probably be the same as the amount of elements you fetch per page.
I have a <FlatList> (from react-native) inside an <Overlay> (from react-native-elements.) I have the problem of onEndReached being executed as soon as the component is rendered for the 1st time and before the user does anything.
The problem was resolved by using <Modal> (from react-native), instead of <Overlay>.
If you are using hooks, here you can find the hook version of #Ilario answer:
const onEndReachedCalledDuringMomentum = useRef(true)
onEndReachedHandler = ({ distanceFromEnd }) => {
if(!onEndReachedCalledDuringMomentum.current){
fetchData()
onEndReachedCalledDuringMomentum.current = true
}
}
<FlatList
...
onEndReached={onEndReachedHandler}
onEndReachedThreshold={0.7}
onMomentumScrollBegin={() => { onEndReachedCalledDuringMomentum.current = false }}
/>
This simple solution worked for me. Note the "refreshing" state is controlled by an async API call in a useEffect hook to retrieve data for the FlatList.
const onEndReachedHandler = () => {
if (!refreshing) ...
}
<FlatList
...
data={mydata}
onEndReached={onEndReachedHandler}
onEndReachedThreshold={0.7}
refreshing={refreshing}
/>
I struggled around the whole day but the issue that I was getting is, I am using FlatList inside ScrollView. So, Remove Scrollview & then use Flatlist independently. This will solve my problem.
From my experience, you can simply utilize onEndReachedThreshold props in your FlatList or SectionList and pass a very very small number like 0.001 to it.
onEndReachedThreshold={0.001}
According to docs for FlatList, onEndReachedThreshold is units of length from the bottom in list items.
How far from the end (in units of visible length of the list) the
bottom edge of the list must be from the end of the content to trigger
the onEndReached callback. For example, a value of 0.5 will trigger
onEndReached when the end of the content is within half the visible
length of the list.
Thus, a very small value like 0.001 helps you to make sure that onEndReached is only gonna be called when the end of the content is within the very end of the visible length of the list.
Hope this helps :) Sorry for bad English.
The solution is simpler than anyone would think.
Just add an !isLoading condition for fetch calling. It works for me:
onEndReached={() => {
if (!isLoading) {
fetchProducts();
}
}}
And the full code with ScrollView and FlatList:
<ScrollView
horizontal={true}
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
style={styles.mainContainer}>
<FlatList
ListHeaderComponent={TableHeader}
data={displayedProducts}
renderItem={(item: ListRenderItemInfo<IProduct>) => TableRow(item, showDeleteModal)}
keyExtractor={keyExtractor}
ListFooterComponent={<Loading loadingText={'Loading products...'} />}
onEndReached={() => {
if (!isLoading) {
fetchProducts();
}
}}
onEndReachedThreshold={0.8}
stickyHeaderIndices={[0]}
/>
</ScrollView>
I have solved it with using debounce from lodash. Firstly, I import debounce from 'lodash.debounce'. Then I use debounce for load more function with 500 ms interval
<Flatlist
onEndReached = {debounce(this._onLoadMore, 500)}
/>

native-base drawer tap to close not taking into account openDrawerOffset

I'm using a Native-Base Drawer component, and I'm setting the openDrawerOffset value myself to be 300 wide (see code below). My main problem is tapToClose is acting as if the drawer is the default width and only responding to a tap within the last 15% of the screen or so.
Is this a bug in native-base? And is there a workaround?
My code is below:
<Drawer
tapToClose={true}
openDrawerOffset={1 - (300 / Dimensions.get('window').width)} // this has to be 300 wide
initializeOpen={true}
ref={(ref) => { this.drawer = ref; }} // not really using this anymore
content={
<SideBarView navigator={this.navigator} closeDrawer={this.closeDrawer.bind(this)} />
}
onClose={() => this.closeDrawer()} >
<MainViewTitlebar
title={this.props.title}
openDrawer={this.openDrawer.bind(this)}
navigation={this.props.navigation}>
</MainViewTitlebar>
<View style={styles.content}>
{this.renderMainView()}
</View>
</Drawer>
You could set the panCloseMask prop same as openDrawerOffset to achieve that.