React native flatlist initial scroll to bottom - react-native

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'
}
};

Related

Is there a FlatList-like View without the scrolling?

Basically, I have a list of items (with images) inside a single Card (custom component). Because the rendering of those items is slow, I wanted to use a FlatList to render them incrementally.
Unfortunately, I get the expected error
VirtualizedLists should never be nested inside plain ScrollViews ...
But I don't actually want to use a ScrollView inside the Card. I just want to render a few Items in a single Card, which should change its size to fit all the items.
Setting scrollEnabled={false} on the FlatList still shows the error above.
Using the ListHeaderComponent and ListFooterComponent props is not an option, because the content above and below should NOT be rendered inside the Card.
Here is a minimal example of what I mean:
const Comp = () => {
return (
<ScrollView contentInsetAdjustmentBehavior="automatic">
<Text>Header</Text>
<Card>
<FlatList
data={data}
renderItem={({ item }) => (
<Image source={{uri: item.localImageUrl}}/>
)}
keyExtractor={(item) => item.id}
scrollEnabled={false}
initialNumToRender={0}
maxToRenderPerBatch={3}
contentInsetAdjustmentBehavior='automatic'
/>
</Card>
<Text>Footer</Text>
</ScrollView>
);
};
What's interesting though, that aside from the error - I get the result I expected, and I could technically hide that error and ignore it, but that does not seem like the recommended approach.
Important: I am not specifically looking for a FlatList solution. It could technically be any Component that renders items incrementally in a non-blocking way.
The important point with a Flatlist is the reusing of cells so that not all components need to be rendered at the same time. So scrolling is an important part of this. On the other hand two scrollable components inside eachother will make it impossible for the system to know which component should be scrolled.
If there are only 3 items and it should not be scrollable you can just include a list of items inside the Scrollview like this:
const Comp = () => {
return (
<ScrollView contentInsetAdjustmentBehavior="automatic">
<Text>Header</Text>
<Card>
{ data.map((item, index) => {
return (
<Text key={index}>{item.title}</Text>
);
}) }
</Card>
<Text>Footer</Text>
</ScrollView>
);
};

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 - Pagination in horizontal FlatList with separators

How to adjust horizontal FlatList with separators in order to skip separators when pagination is enabled. I want to see separators only when swiping between items. I tried to set it in getItemLayout but it doesn't work properly. I used getItemLayout = (_, index) => ({ length: window.width, offset: (window.width + separatorWidth) * index, index }) Behaviour looks like that
This confused me as well.
There are a few snap related properties inherited/extended from <ScrollView> that are useful here.
Checkout: snapToInterval and snapToOffsets.
If you're using a <FlatList> or <ScrollView> to act as a horizontal full-width carousel, and want to enforce snapping so that a single "page" within the list is always within view (i.e. users can't stop partially between views), these snap props are what you need.
Note: you need to disable pagingEnabled in order for these props to be respected.
Simplified example code:
render() {
const totalItemWidth = window.width + separatorWidth;
return (
<FlatList
{ /* ... other props — data, renderItem, style, etc ... */}
horizontal
showsHorizontalScrollIndicator={false}
snapToInterval={totalItemWidth}
decelerationRate="fast"
bounces={false}
getItemLayout={(data, index) => ({
length: totalItemWidth,
offset: totalItemWidth * index,
index,
})}
ItemSeparatorComponent={SomeSeparatorComponent}
/>
)
}
How it behaves on iOS verse Android:
Android is a bit clunkier and I'm still refining the decelerationRate and overall feel... but it's close, IMO.

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)}
/>