React Native SectionList scrollToLocation not working inside a scrollview - react-native

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.

Related

ERROR - VirtualizedLists should never be nested inside plain ScrollViews with the same orientation

I'm working on a react-native app and I have to put a list of object in a Scrollview, so I use the FlatList component to do it. This is the piece of code that generates the error:
<ScrollView contentContainerStyle={style}>
Other components
<FlatList
style={style}
data={data}
scrollEnabled={false}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item, index}) => (somethings)}
/>
Other components
</ScrollView>
The complete error is: VirtualizedLists should never be nested inside plain ScrollViews with the same orientation because it can break windowing and other functionality - use another VirtualizedList-backed container instead.
Avoid using FlatList with the same orientation. Instead, restructure your code like this --
<ScrollView contentContainerStyle={style}>
Other components
{
data.map((item)=> <Somthing item={item}/>)
}
Other components
</ScrollView>
Flatlist has its own ScrollView you can scroll through the list using that so there is no need to put a flatlist into a ScrollView that is why its giving a warning, the both scrollview will clash and one of them (mostly the parent one) works.
The error is self explanatory and it should be in a developers best interest to avoid these kind of things even when it's just a false alarm.
Your particular situation could use the following solution:
<FlatList
data={data}
keyExtractor={(item, index) => `key-${index}`}
ListHeaderComponent={() => (
<SomeComponents>
...Some components those need to be on top of the list
</SomeComponents>
)}
ListFooterComponent={() => (
<SomeComponents>
...Some components those need to be below the list
</SomeComponents>
)}
renderItem={({ item, index}) => (somethings)}
/>
Another note, if you need more complex list that needs header and footer for the list itself, you can try SectionList.
Your component FlatList and ScrollView have the same orientation(vertical), so you need put your component inside a ScrollView with horizontal orientation like this:
<View>
<ScrollView nestedScrollEnabled={true} style={{ width: "100%" }} >
<View>
<ScrollView horizontal={true} style={{ width: "100%" }}>
<FlatList />
</ScrollView>
</View>
</ScrollView>
</View>
Solution 1: Use FlatList props ListHeaderComponent and create all of your page top section in that. Something like this:
This will not show any warning or error.
Solution 2:
Because only parent view will scroll (ScrollView) and not the child FlatList, so to get rid of the warning you can pass a prop scrollEnabled={false} to the FlatList.
If it doesn't go then import LogBox from react-native and write this in your component
useEffect(() => {
LogBox.ignoreLogs(["VirtualizedLists should never be nested"])
}, [])
hopefully, the warning will be removed.
Anyone want to solve this issue can use a custom VirtualizedScrollView like this:
import React from 'react';
import { FlatList } from 'react-native';
const VirtualizedScrollView = props => {
return (
<FlatList
{...props}
data={[]}
keyExtractor={(e, i) => 'dom' + i.toString()}
ListEmptyComponent={null}
renderItem={null}
ListHeaderComponent={() => (
<>{props.children}</>
)}
/>
);
};
export default VirtualizedScrollView;
Then if you use FlatList inside VirtualizedScrollView, it won't get the warning/error.
<VirtualizedScrollView>
<FlatList
/*--- your props ---*/
/>
</VirtualizedScrollView>
There is a npm package where I get this code, you can also use this package
Solution:
I have also encountered same problem with FlatList. Then the package below solved my problem.
'react-native-virtualized-view'
import { ScrollView } from 'react-native-virtualized-view'
if ScrollView is Vertical change Flatlist Horizontal
<ScrollView >
<FlatList
horizontal
data={lenders}
keyExtractor={(_, index) => index}
renderItem={(item) => {
return <Text>item</Text>
}}
/>
You can solve the 2 vertical ones(I'm assuming their side by side, separated with a segemented control?) by using the same flat list and switching out the data when it's switched. If they're just two vertical flat list's one after another use the SectionList.
For the horizontal one you can try putting the Horizontal FlatList in the ListHeaderComponent of the vertical FlatList and see what happens. It can be janky if you use a vertical FlatList in a vertical scroll view but maybe with two different axis it might be ok. The other option is two only show a few items in the horizontal scrollview and have a "Show More".
The last option is too re design/rethink the page so it's not doing so much. On mobile less is more and developers/designers like to get in the mindset of porting desktop thinking onto mobile. Might be worth a shot.
I used the SectionList approach to solve this & wanted to post a code example because I found the Section data required by React Native to be clear but also quite prescriptive.
renderList = ({empty, posts}: {empty: boolean, posts: Array<Object>}) => (
<SectionList
sections={[
{type: 'MAP', data: [{}]}, // Static sections.
{type: 'PROFILE', data: [{}]},
{type: 'POSTS', data: posts} // Dynamic section data replaces the FlatList.
]}
keyExtractor={(item, index) => index}
renderItem={({item, section}) => {
switch (section.type) {
// Different components for each section type.
case 'MAP':
return <MapView />;
case 'PROFILE':
return <Profile />;
case 'POSTS':
return <Post item={item} />;
default:
return null;
}
}}
ItemSeparatorComponent={() => <Separator />}
ListFooterComponent={() => <>{empty && <EmptyList />}</>}
/>
);
What's nice is that the content feels logically quite separate, so you can add sections easily or have different dynamic data sources.
(If you're building a form & want better keyboard handling, you could also try a KeyboardAwareSectionList from react-native-keyboard-aware-scroll-view.)
Flatlist has an integrated scrollview itself, so you can resolve this error by removing ScrollView Component, And let just the Fatlist component
Error ? you are trying to render a FlatList component inside a scrollview component, this is what is throwing the warning.
solution Render the components using Flatlist's ListHeaderComponent={} prop, i.e in your flatlist add the prop as follows
const FlatList_Header = () => {
return (
<View style={{
height: 45,
width: "100%",
backgroundColor: "#00B8D4",
justifyContent: 'center',
alignItems: 'center'
}}
>
<Text style={{ fontSize: 24, color: 'white' }}> Sample FlatList Header </Text>
</View>
);
}
<FlatList
data={BirdsName}
renderItem={({ item }) => <ItemRender name={item.name} />}
keyExtractor={item => item.id}
ItemSeparatorComponent={ItemDivider}
**ListHeaderComponent={FlatList_Header}**
ListHeaderComponentStyle={{ borderBottomColor: 'red', borderBottomWidth: 2 }}
/>
Note the use of the ListHeaderComponent in the code above, that should supress the warning.
Use flatList like this ListHeaderComponent and ListFooterComponent:
<FlatList ListHeaderComponent={
<ScrollView
style={styles.yourstyle}
showsVerticalScrollIndicator={false}
>
<View style={styles.yourstyle}>
</View>
</ScrollView>
}
data={this.state.images}
renderItem={({ item, index }) => {
return (
<View
style={styles.yourstyle}
>
<Image
source={{
uri: item,
}}
style={styles.yourstyle}
resizeMode={"contain"}
/>
<Text
numberOfLines={2}
ellipsizeMode="tail"
style={styles.yourstyle}
>
{item.name}
</Text>
</View>
);
}}
keyExtractor={({ name }, index) => index.toString()}
ListFooterComponent={
<View style={styles.yourstyle}></View>
}
/>
In my case it was happening due to nesting of ScrollView.
Try replacing some of the ScrollView from children components with React.Fragment.
The solution is very simple, please do not put the Flatlist component in the ScrollView.
They both have the same functionality but Flatlist has advantages and is more stable to use.

React Native FlatList onViewableItemsChanged Returning Incorrect set of Items

I am trying to use onViewableItemsChanged event to detect the items of my FlatList that are currently displayed on screen.
In my ViewabilityConfig (Code is provided below), I set the itemVisiblePercentThreshold parameter to 100 which I assumed will require my item to be fully displayed to be considered viewable. However that is not the case for me.
As you can see in the following screenshot:
Screenshot of my App
It is obvious that the top most item is not completely on screen (Which should make the visible items consist only of 3 items). But when I print the length of the array in my onViewableItemsChanged event handler, it returns 4 (and when I inspect the values, including the top most item).
Log Result of Viewable Items Array Length
Is this the problem of FlatList onViewableItemsChanged event? Or did I implemented it incorrectly?
I tried to find solution from the documentation and React-native github but there is no further explanation about how this event works.
Some related snippets of my code are as follow:
FlatList Definition
<FlatList
viewabilityConfig={this.clippingListViewabilityConfig}
inverted={true}
horizontal={false}
data = {this.props.clippingResultArray}
ref={(ref) => this.clippingResultFlatList = ref}
style={{
// flexGrow:0,
// backgroundColor: 'green',
// width:'100%',
// width: Dimensions.get('window').width,
}}
contentContainerStyle={{
// justifyContent:'flex-end',
// flexGrow:0,
// flexDirection:'row',
// alignItems:'flex-end',
}}
renderItem={this.renderClippingListItemRight}
keyExtractor={(item, index) => index.toString()}
onViewableItemsChanged={this.onClippingListViewableChanged}
// removeClippedSubviews={true}
{...this._clippingListItemPanResponder.panHandlers}
/>
onViewableItemsChanged Listener
onClippingListViewableChanged = (info) => {
console.log("***************************NUMBER OF CURRENT VIEWABLE ITEMS:",info.viewableItems.length);
console.log("Item list:",info.viewableItems);
this.setState({
...this.state,
viewableItems: info.viewableItems,
});
};
Viewable Configuration
this.clippingListViewabilityConfig = {
waitForInteraction: false,
itemVisiblePercentThreshold: 100,
minimumViewTime: 500, //In milliseconds
};

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

React Native <ScrollView> persistent scrollbar

After perusing the React Native Documentation I couldn't seem to find out how to make a <ScrollView> have a persistent scrollbar that doesn't fade out. How would I achieve that?
iOS
The underlying iOS native component, UIScrollView (technically, RCTEnhancedScrollView), doesn't support keeping the scroll indicators visible. For this reason, the React Native wrapper around it won't either.
There is a hack to get this working with the native component (see this answer for one approach). To accomplish this in React Native, you'd need to implement this hack on the native side, and then either create your own Native Module or fork React Native and modify their ScrollView component.
That said, the iOS Scroll View interface guidelines discourage this, so you may want to leave the indicators' behavior alone.
Android
A few approaches:
set <item name="android:overScrollMode">always</item>,
set android:fadeScrollbars="false" in XML, or
set ScrollView.setScrollbarFadingEnabled(false) in Java (e.g. in your custom native bridge code)
This is similarly discouraged as nonstandard UI unless you have a strong reason for it.
Adding answer since none of the above worked for me.
Android now has the persistentScrollbar props.
iOS does not support this. So I created a JS solution that can be used as follows:
<SBScrollView persistentScrollbar={true}>...</SBScrollView>
Basically, this functional component will use persistentScrollbar when on Android, while add a bar when we are on iOS. It is not smooth for now, but it is functional.
// #flow
import React, {useState} from 'react';
import {Platform, View, ScrollView} from 'react-native';
type Props = {|
persistentScrollbar?: boolean,
children?: React$Node,
|} & View.propTypes;
export default function SBScrollView({
persistentScrollbar = false,
children,
...other
}: Props) {
const [nativeEvent, setNativeEvent] = useState();
if (Platform.OS === 'android' || !persistentScrollbar) {
// Abdroid supports the persistentScrollbar
return (
<ScrollView persistentScrollbar={persistentScrollbar} {...other}>
{children}
</ScrollView>
);
}
const top = nativeEvent
? nativeEvent.contentOffset.y +
(nativeEvent.contentOffset.y / nativeEvent.contentSize.height) *
nativeEvent.layoutMeasurement.height
: 0;
// iOS does not support persistentScrollbar, so
// lets simulate it with a view.
return (
<ScrollView
scrollEventThrottle={5}
showsVerticalScrollIndicator={false}
onScroll={event => setNativeEvent(event.nativeEvent)}
{...other}>
{children}
<View
style={{
position: 'absolute',
top,
right: 4,
height: 200,
width: 4,
borderRadius: 20,
backgroundColor: 'gray',
}}
/>
</ScrollView>
);
}
I hope this can help others.
I was looking for a solution but I didn't find nothing, then I created a solution, I hope can help you with it.
I created a view View with height and width and put it over my scrollview, after that I used the Props of scrollview like onMomentumScrollBegin, onMomentumScrollEnd, onContentSizeChange and onScroll
after that I make a condition with a boolean variable, if this variable is false, the View is visible, if is false the View is hide, How do I active this variable? with the Prop onMomentumScrollBegin that detect when you use the scrollView and the same way to set the variable in false with onMomentumScrollEnd that detects when the scroll ends.
The Prop onContentSizeChange allows me to get the height and width of my scrollview, this values I used to calculate where would be set the scrollbar/scrollIndicator
and finally with the Prop onScroll I get the position.
the example:
<ScrollView
onMomentumScrollBegin={() => {this.setvarScrollState()}}
onMomentumScrollEnd={() => {this.setvarScrollStateRev()}}
scrollEventThrottle={5}
onContentSizeChange={(w, h) => this.state.hScroll = h}
showsVerticalScrollIndicator={false}
onScroll={event => { this.state.wScroll = event.nativeEvent.contentOffset.y }}
style={{ marginVertical: 15, marginHorizontal:15, width: this.state.measurements3.width}}>
{
Mydata.map((value, index) => {
return <TouchableOpacity>
<Text>{ value.MyDataItem }</Text>
</TouchableOpacity>
})
}
the functions:
setvarScrollState() {
this.setState({VarScroll: true});
}
setvarScrollStateRev() {
this.setState({VarScroll: false});
}
and the variable
this.state = {VarScroll: false}
Then my condition is
!this.state.VarScroll ?
<View
style = {{
marginTop: 200*(this.state.wScroll / this.state.hScroll),
marginLeft:338.5,
height: 35,
width: 2,
backgroundColor: 'grey',
position:'absolute'
}}
/>
: null
Why 200? because is the maximum value that my marginTop can set
Check the picture
Final note:
the scrollView have to be inside a View with the another View (scrollbar)
something like this
<View>
{/*---- ScrollBar and conditions----*/}
<View>
<View>
<ScrollView>
</ScrollView>
</View>