Why is SectionList ref.current always undefined? - react-native

I am trying to use the scrollToOffset and scrollToIndex that SectionList provides. However, the SectionList ref I am using is always undefined when using ref.current.
In my class component I have the following:
private sectionListRef: any;
constructor(props: SampleScreenProps) {
super(props);
this.sectionListRef = null;
}
componentDidMount = (): void => {
setTimeout(() => this.scrollToSection(), 2000);
};
scrollToSection = (): void => {
// do some calculations to get the itemIndex and sectionIndex to scroll to
this.sectionListRef.scrollToLocation({
animated: false,
itemIndex,
sectionIndex,
viewPosition: 0.5
}); // this fails if the list is huge and the indices are far from the initial render location, and so moves on to onScrollToIndexFailed
};
onScrollToIndexFailed = (error) => {
// this.sectionListRef.current is always undefined in this function
this.sectionListRef.current?.scrollToOffset({
offset: error.averageItemLength * error.index,
animated: true
});
setTimeout(() => {
if (this.state.dataSet.length !== 0 && this.sectionListRef !== null) {
this.sectionListRef.current?.scrollToIndex({ index: error.index, animated: true });
}
}, 10);
};
finally, here's the SectionList itself:
<SectionList
inverted
ref={(ref) => {
this.sectionListRef = ref;
}}
sections={mappedSections}
renderItem={this.renderItem}
renderSectionFooter={this.renderSectionFooter}
keyExtractor={(item, index) => index.toString()}
keyboardShouldPersistTaps="never"
keyboardDismissMode="on-drag"
bounces={false}
initialNumToRender={20}
onScrollToIndexFailed={this.onScrollToIndexFailed}
/>
What is the cause of this?
Edit: I tried doing this.sectionListRef = React.createRef() in the constructor instead, but same result.
I understand scrollToOffset is part of SectionList, but the other two scroll functions are part of the underlying virtualized list, but it should still work the same as far as I know.

Related

when FlatList inside FlatList or ScrollView , I got a TypeError

class TestClass extends PureComponent<Props> {
private leftFilterFlatListRef = null;
private rightFilterFlatListRef = null;
private setLeftFilterFlatListRef = ref => {
this.leftFilterFlatListRef = ref;
};
private setRightFilterFlatListRef = ref => {
this.rightFilterFlatListRef = ref;
};
private onRightFilterGroupItemTitleClickInner = index => {
this.rightFilterFlatListRef &&
this.rightFilterFlatListRef.scrollToIndex({
animated: true,
index,
viewPosition: 0
});
};
private onLeftFilterItemClickInner = index => {
console.log('======context====' + JSON.stringify(this.rightFilterFlatListRef.scrollToIndex)); // undefined
this.rightFilterFlatListRef &&
this.rightFilterFlatListRef.scrollToIndex({
animated: true,
index,
viewPosition: 0
});
};
public leftFilterScrollTo = index => {
this.leftFilterFlatListRef &&
this.leftFilterFlatListRef.scrollToIndex({
animated: true,
index: index,
viewPosition: 1
});
};
public rightFilterScrollTo = index => {
this.rightFilterFlatListRef &&
this.rightFilterFlatListRef.scrollToIndex({
animated: true,
index,
viewPosition: 0
});
};
private renderLeftFilterItem = () => {
return <LeftItemComponent item={item} index={index} onLeftFilterItemClick={this.onLeftFilterItemClickInner} />;
};
private renderRightFilterItem = () => {
return (
<RightItemComponent
index={index}
item={item}
onRightFilterGroupItemTitleClick={this.onRightFilterGroupItemTitleClickInner}
/>
);
};
private renderLeftFlatItem = ({ item, index }) => {
return this.renderLeftFilterItem(this.props, item, index, this.props.onVisible);
};
private renderRightFlatItem = ({ item, index }) => {
return this.renderRightFilterItem(this.props, item, index, this.props.onVisible);
};
render(): React.ReactNode {
const { leftFilterData, rightFilterData } = this.props;
const nonNullLeftFilterData = leftFilterData || [];
const nonNullRightFilterData = rightFilterData || [];
return (
<View>
<FlatList
data={nonNullLeftFilterData}
ref={this.setLeftFilterFlatListRef}
renderItem={this.renderLeftFlatItem}
/>
<FlatList
data={nonNullRightFilterData}
ref={this.setRightFilterFlatListRef}
renderItem={this.renderRightFlatItem}
/>
</View>
);
}
}
I got the Error message
TypeError: undefined is not a function (near '...this._scrollRef.scrollTo...')
When I put TestClass inside a FlatList or ScrollView. I found that the Ref.scrollToIndex from FlatList was undefined. But I want to get the scrollToIndex function, and it should be executed rightly.
Thank you all . I understand why this.rightFilterFlatListRef.scrollToIndex can not be executed rightly. there is some code in VirtualizedList.
_defaultRenderScrollComponent = props => {
const onRefresh = props.onRefresh;
if (this._isNestedWithSameOrientation()) {
// $FlowFixMe - Typing ReactNativeComponent revealed errors
return <View {...props} />;
} else if (onRefresh) {
invariant(
typeof props.refreshing === 'boolean',
'`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' +
/* $FlowFixMe(>=0.111.0 site=react_native_fb) This comment suppresses
* an error found when Flow v0.111 was deployed. To see the error,
* delete this comment and run Flow. */
JSON.stringify(props.refreshing) +
'`',
);
return (
// $FlowFixMe Invalid prop usage
<ScrollView
{...props}
refreshControl={
props.refreshControl == null ? (
<RefreshControl
refreshing={props.refreshing}
onRefresh={onRefresh}
progressViewOffset={props.progressViewOffset}
/>
) : (
props.refreshControl
)
}
/>
);
} else {
// $FlowFixMe Invalid prop usage
return <ScrollView {...props} />;
}
};
when FlatList A is inside FlatList B, FlatList A becomes View
we add <ScrollView {...props} /> as a prop of FlatList A. Then FlatList A can scroll again.

React Native dynamic search with flatlist from API

function ManageData({props, navigation}) {
const [details, setDetails] = useState({
dataList: [],
loading: true,
offset: 1,
totalRecords: 0,
search: '',
});
useEffect(() => {
getData();
}, []);
const getData = async () => {
try {
// console.log('search',details.search);
var params = {};
params = {
'pagination[page]': details.offset,
'pagination[perpage]': 10,
};
if(details?.search?.length > 0){
params['query[search]'] = details?.search;
params['pagination[pages]'] = 30;
params['pagination[total]'] = 293;
}else{
params['query'] = ""
}
const result = await getPayeeDetails(session, params);
// console.log('result',result?.data?.data?.length);
if (result?.data?.data?.length > 0) {
setDetails(prev => ({
...prev,
offset: prev.offset + 1,
dataList: [...prev.dataList, ...result.data.data],
loading: false,
totalRecords: result.data.recordsFiltered,
}));
}
} catch (error) {
console.log('getPayeesError', error);
}
};
const loadMore = () => {
try {
if (details.dataList.length != details.totalRecords) {
setDetails(prev => ({
...prev,
loading: true,
}));
getData();
}
} catch (error) {
console.log('LoadMoreError', error);
}
};
const searchHandler=(data)=>{
try{
console.log('clearData',data);
setDetails(prev => ({
...prev,
dataList:[],
offset:1,
search: data == 'RESET'?"":data,
}));
getData();
}catch(error){
console.log("SearchError",error)
}
}
return (
<BackDropContainer
searchHandler={searchHandler}>
<View style={{backgroundColor: 'white', flex: 1}}>
<FlatList
style={{marginTop: '4%'}}
data={details?.dataList}
renderItem={({item}) => (
<TouchableOpacity onPress={() => showDialog(item)}>
<Item data={item} />
</TouchableOpacity>
)}
onEndReached={loadMore}
keyExtractor={(item, index) => index}
/>
</View>
</BackDropContainer>
);
}
I have a flatlist with searchview in my React Native application. Each time user scrolls to the end of flatlist the loadmore function will be called and also the offset value is increased as 1 to fetch next page from API.
Every time the API results array of 10 data from API so the flatlist will be loaded 10 by 10 for each scroll. When I type some data in searchview the searchHandler function will be called, and there I want to reset the offset as 1 and also need to send typed data to the API.
The issue is searched data and offset is not sending with API whenever I try to search the data. State is not updating properly when searching data.
Note: The data which is types has to be sent along with API whenever user search something.

How to scroll FlatList to some index immediately after scrolling?

How to scroll FlatList component to some index/children after dragging the FlatList?
For Example:
As we can see in Youtube/TikTok stories, when we drag the screen the next video appears immidiatly after it. So, I am implementing it with FlatList, if we drag the item below then FlatList should move to the above item/index. So, what I am doing is that storing the currently displayed index and on onScrollEndDrag prop I am checking the position of Y, and accordingly run scrollToIndex function, but it's not working.
Reason
Because while scrolling after drag, FlatList ignores the scrollToIndex function.
Is their anyone to help me out of it???
import React from 'react';
import { View, Text, StyleSheet, AppState, FlatList, Animated, Dimensions } from 'react-native';
import fetchDataFromDirectory from '../data/fetchDataFromWhatsApp';
import PlayerVideo from '../components/VideoPlayer';
import Image from '../components/Image';
const { width, height } = Dimensions.get('window');
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
class VideoScreen extends React.Component {
state = {
pdfInfo: [], //[{id, name, path},...]
appState: '',
viewableIndex: 0
}
fetchData = async () => {
const data = await fetchDataFromDirectory('videos');
this.setState({ pdfInfo: data.pdfInfo });
}
componentDidUpdate(prevProps, prevState) {
if (this.state.pdfInfo.length > this.dataLength) { //We are seeing if we need to scroll to top or not
this.dataLength = this.state.pdfInfo.length;
try {
this.list.scrollToIndex({ animated: true, index: 0, viewPosition: 0 })
} catch (err) {
}
}
}
handleAppStateChange = (nextAppState) => {
//the app from background to front
if (this.state.appState.match(/inactive|background/) && nextAppState === 'active') {
this.fetchData();
}
//save the appState
this.setState({ appState: nextAppState });
}
componentDidMount() {
this.videoHeight = height;
this.dataLength = 0;
this.fetchData();
AppState.addEventListener('change', this.handleAppStateChange);
}
onViewableItemsChanged = ({ viewableItems, changed }) => {
// console.log("Visible items are", viewableItems);
// console.log("Changed in this iteration", changed);
try {
this.setState({ viewableIndex: viewableItems[0]['index'] })
} catch (err) {
}
}
componentWillUnmount() {
AppState.removeEventListener('change', this.handleAppStateChange)
}
render() {
return <AnimatedFlatList
onLayout={(e) => {
const { height } = e.nativeEvent.layout;
this.videoHeight = height;
}}
// onResponderRelease={e => {console.log(e.nativeEvent.pageY)}}
// onResponderRelease={(e) => console.log(e.nativeEvent.)}
// onScrollBeginDrag
// snapToAlignment={'top'}
// onMoveShouldSetResponderCapture={(e) => {e.nativeEvent.}}
// decelerationRate={'fast'}
decelerationRate={'fast'}
scrollEventThrottle={16}
// onScroll={(e) => console.log('+++++++++++++++++',Object.keys(e), e.nativeEvent)}
onScrollEndDrag={(e) => {
// this.list.setNativeProps({ scrollEnabled: false })
console.log(e.nativeEvent)
if (e.nativeEvent.velocity.y > 0.1) {
console.log('go to above')
this.list.scrollToIndex({animated: true, index: this.state.viewableIndex - 1, viewPosition: 0})
} else if (e.nativeEvent.velocity.y < -0.9){
console.log('go to below')
this.list.scrollToIndex({animated: true, index: this.state.viewableIndex + 1, viewPosition: 0})
}
// if (e.nativeEvent.velocity.y < 0.1000 && e.nativeEvent.velocity.y >= 0) {
// this.list.scrollToIndex({animated: true, index: this.state.viewableIndex, viewPosition: 0})
// }
// else if (e.nativeEvent.velocity.y < -0.1000 && e.nativeEvent.velocity.y < 0) {
// this.list.scrollToIndex({animated: true, index: this.state.viewableIndex, viewPosition: 0})
// }
// this.list.setNativeProps({ scrollEnabled: true })
console.log('h1')
}}
viewabilityConfig={{
// itemVisiblePercentThreshold: 90,
viewAreaCoveragePercentThreshold: 60
}}
// extraData={this.state.viewableIndex}
onViewableItemsChanged={this.onViewableItemsChanged}
// scr
contentContainerStyle={styles.screen}
data={this.state.pdfInfo}
keyExtractor={item => item.id}
ref={ref => this.list = ref}
renderItem={({ item, index }) => {
// console.log(index)
return <PlayerVideo
source={item.path}
refList={this.list}
height={this.videoHeight}
index={index}
isViewable={this.state.viewableIndex == index ? true : false} />
}}
</View>
}}
/>
}
}
export default VideoScreen;
const styles = StyleSheet.create({
screen: {
backgroundColor: '#111212',
// flex: 1
}
})
First add a ref flatListRef to the flatlist:
<Flatlist
ref={(ref) => this.flatListRef = ref}
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
onScrollBeginDrag={onBeginScroll}/>
/>
Then define the onScrollBeginDrag function as below to scroll to a specific index:
onBeginScroll = () => {
this.flatListRef._listRef._scrollRef.scrollToIndex({ animating: true, index: [YOUR_INDEX_HERE] });
}
yes you should access the element _listRef then _scrollRef then call the scrollToIndex
react-native 0.64.1
react 17.0.2

FlatList items re-rendering even with React.memo

I am trying to render a list of items in React Native with the FlatList component but every time I fetch new data it re-renders the who list of items even with React.memo.
Here is what my code looks like:
const data = [
{ _id: 1, text: 'Hello World' },
{ _id: 2, text: 'Hello' },
{ ... }
]
const renderItem = ({ item }) => (<Component item={item} />)
const loadMore = () => {
//Fetching data from db and adding to data array
}
<FlatList
data={data}
keyExtractor={item => item._id}
renderItem={renderItem}
onEndReached={loadMore}
removeClippedSubviews={true}
/>
Component.js
const Component = ({ item }) => {
console.log('I am rendering')
return (
<Text>{item.text}</Text>
)
}
const equal = (prev, next) => {
return prev.item.text === next.item.text
}
export default React.memo(Component, equal)
Every time the onEndReached function gets triggered and calls the loadMore function, all FlatList items get re-rendered, it console.log 'I am rendering' every single time and causes the error virtualizedlist you have a large list that is slow to update
Thanks to anyone who can help me!
I don't know why but I fixed it with an if statement in the equal function
//Changing this
const equal = (prev, next) => {
return prev.item.text === next.item.text
}
//To this
const equal = (prev, next) => {
if(prev.item.text !== next.item.text) {
return false;
}
return true
}
Hope this could help someone else.

Scrolling issues with FlatList when rows are variable height

I'm using a FlatList where each row can be of different height (and may contain a mix of both text and zero or more images from a remote server).
I cannot use getItemLayout because I don't know the height of each row (nor the previous ones) to be able to calculate.
The problem I'm facing is that I cannot scroll to the end of the list (it jumps back few rows when I try) and I'm having issues when trying to use scrollToIndex (I'm guessing due to the fact I'm missing getItemLayout).
I wrote a sample project to demonstrate the problem:
import React, { Component } from 'react';
import { AppRegistry, StyleSheet, Text, View, Image, FlatList } from 'react-native';
import autobind from 'autobind-decorator';
const items = count => [...Array(count)].map((v, i) => ({
key: i,
index: i,
image: 'https://dummyimage.com/600x' + (((i % 4) + 1) * 50) + '/000/fff',
}));
class RemoteImage extends Component {
constructor(props) {
super(props);
this.state = {
style: { flex: 1, height: 0 },
};
}
componentDidMount() {
Image.getSize(this.props.src, (width, height) => {
this.image = { width, height };
this.onLayout();
});
}
#autobind
onLayout(event) {
if (event) {
this.layout = {
width: event.nativeEvent.layout.width,
height: event.nativeEvent.layout.height,
};
}
if (!this.layout || !this.image || !this.image.width)
return;
this.setState({
style: {
flex: 1,
height: Math.min(this.image.height,
Math.floor(this.layout.width * this.image.height / this.image.width)),
},
});
}
render() {
return (
<Image
onLayout={this.onLayout}
source={{ uri: this.props.src }}
style={this.state.style}
resizeMode='contain'
/>
);
}
}
class Row extends Component {
#autobind
onLayout({ nativeEvent }) {
let { index, item, onItemLayout } = this.props;
let height = Math.max(nativeEvent.layout.height, item.height || 0);
if (height != item.height)
onItemLayout(index, { height });
}
render() {
let { index, image } = this.props.item;
return (
<View style={[styles.row, this.props.style]}>
<Text>Header {index}</Text>
<RemoteImage src = { image } />
<Text>Footer {index}</Text>
</View>
);
}
}
export default class FlatListTest extends Component {
constructor(props) {
super(props);
this.state = { items: items(50) };
}
#autobind
renderItem({ item, index }) {
return <Row
item={item}
style={index&1 && styles.row_alternate || null}
onItemLayout={this.onItemLayout}
/>;
}
#autobind
onItemLayout(index, props) {
let items = [...this.state.items];
let item = { ...items[index], ...props };
items[index] = { ...item, key: [item.height, item.index].join('_') };
this.setState({ items });
}
render() {
return (
<FlatList
ref={ref => this.list = ref}
data={this.state.items}
renderItem={this.renderItem}
/>
);
}
}
const styles = StyleSheet.create({
row: {
padding: 5,
},
row_alternate: {
backgroundColor: '#bbbbbb',
},
});
AppRegistry.registerComponent('FlatListTest', () => FlatListTest);
Use scrollToOffset() instead:
export default class List extends React.PureComponent {
// Gets the total height of the elements that come before
// element with passed index
getOffsetByIndex(index) {
let offset = 0;
for (let i = 0; i < index; i += 1) {
const elementLayout = this._layouts[i];
if (elementLayout && elementLayout.height) {
offset += this._layouts[i].height;
}
}
return offset;
}
// Gets the comment object and if it is a comment
// is in the list, then scrolls to it
scrollToComment(comment) {
const { list } = this.props;
const commentIndex = list.findIndex(({ id }) => id === comment.id);
if (commentIndex !== -1) {
const offset = this.getOffsetByIndex(commentIndex);
this._flatList.current.scrollToOffset({ offset, animated: true });
}
}
// Fill the list of objects with element sizes
addToLayoutsMap(layout, index) {
this._layouts[index] = layout;
}
render() {
const { list } = this.props;
return (
<FlatList
data={list}
keyExtractor={item => item.id}
renderItem={({ item, index }) => {
return (
<View
onLayout={({ nativeEvent: { layout } }) => {
this.addToLayoutsMap(layout, index);
}}
>
<Comment id={item.id} />
</View>
);
}}
ref={this._flatList}
/>
);
}
}
When rendering, I get the size of each element of the list and write it into an array:
onLayout={({ nativeEvent: { layout } }) => this._layouts[index] = layout}
When it is necessary to scroll the screen to the element, I summarize the heights of all the elements in front of it and get the amount to which to scroll the screen (getOffsetByIndex method).
I use the scrollToOffset method:
this._flatList.current.scrollToOffset({ offset, animated: true });
(this._flatList is ref of FlatList)
So what I think you can do and what you already have the outlets for is to store a collection by the index of the rows layouts onLayout. You'll want to store the attributes that's returned by getItemLayout: {length: number, offset: number, index: number}.
Then when you implement getItemLayout which passes an index you can return the layout that you've stored. This should resolve the issues with scrollToIndex. Haven't tested this, but this seems like the right approach.
Have you tried scrollToEnd?
http://facebook.github.io/react-native/docs/flatlist.html#scrolltoend
As the documentation states, it may be janky without getItemLayout but for me it does work without it
I did not find any way to use getItemLayout when the rows have variable heights , So you can not use initialScrollIndex .
But I have a solution that may be a bit slow:
You can use scrollToIndex , but when your item is rendered . So you need initialNumToRender .
You have to wait for the item to be rendered and after use scrollToIndex so you can not use scrollToIndex in componentDidMount .
The only solution that comes to my mind is using scrollToIndex in onViewableItemsChanged . Take note of the example below :
In this example, we want to go to item this.props.index as soon as this component is run
constructor(props){
this.goToIndex = true;
}
render() {
return (
<FlatList
ref={component => {this.myFlatList = component;}}
data={data}
renderItem={({item})=>this._renderItem(item)}
keyExtractor={(item,index)=>index.toString()}
initialNumToRender={this.props.index+1}
onViewableItemsChanged={({ viewableItems }) => {
if (this.goToIndex){
this.goToIndex = false;
setTimeout(() => { this.myFlatList.scrollToIndex({index:this.props.index}); }, 10);
}
}}
/>
);
}
You can use onScrollToIndexFailed to avoid getItemLayout
onScrollToIndexFailed={info => {
const wait = new Promise(resolve => setTimeout(resolve, 100));
wait.then(() => {
refContainer.current?.scrollToIndex({
index: pinPosition || 0,
animated: true
});
});
}}