Crash when doing scrollToLocation on SectionList - reactjs

We have an edge case in our app. After the UI is rendered and the user tries to scroll to a section it throws scrolltoindex should be used in conjunction with getitemlayout or on scrolltoindex failed. Now this happens only when he does this immediately after UI render.
_scrollToSection = index => {
setTimeout(() => {
this.list.scrollToLocation({
animated: true,
itemIndex: -1,
sectionIndex: index,
viewPosition: 0
});
}, 150);
};
Section list render:
<SectionList
sections={this.props.sections}
extraData={this.props.subscriber}
ref={ref => {
if (ref) {
this.list = ref;
}
}}
automaticallyAdjustContentInsets={true}
contentInsetAdjustmentBehavior={'automatic'}
windowSize={100}
ListHeaderComponent={this.props.header || null}
ItemSeparatorComponent={() => (
<Separator
style={[mediumStyle.separatorEnd, { backgroundColor: IOS_GREY_02_03 }]}
/>
)}
renderSectionFooter={() => <View style={{ height: 17 }} />}
keyExtractor={(item, index) => index}
removeClippedSubviews={false}
stickySectionHeadersEnabled={true}
renderSectionHeader={({ section }) => (
<SectionTitle title={section.title} theme={this.props.theme} />
)}
renderItem={this._renderItem}
onEndReachedThreshold={0}
onEndReached={() => HapticFeedback.trigger()}
scrollEventThrottle={16}
/>
I tried to google the cause but was unsuccessful finding only outdated and closed issues without a solution. Did this happen to anybody else? How did you fixed it?
UPDATE:
We have come up on a solution of constant item sizes which also takes in count the accessibility scale factor. So we had an item and header size we could use in getItemLayout. All worked as should, but the SectionList is glitchy. When we scrolled to lower sections, the list was jumpy by itself without any interaction.
So far the best solution we had was to build the section list ourselves in native code and use that instead of the RN list.

You're getting this error because scrollToIndex has failed and you have not implemented getItemLayout or onScrollToIndexFailed
getItemLayout in a section list is not exactly fun to setup however this medium post goes over how to do it https://medium.com/#jsoendermann/sectionlist-and-getitemlayout-2293b0b916fb
They suggest react-native-section-list-get-item-layout to calculate the sizes of the layout https://github.com/jsoendermann/rn-section-list-get-item-layout
onScrollToIndexFailed is easier to setup you can add the prop onScrollToIndexFailed={(info) => { /* handle error here /*/ }} You can catch the error and then decide on how you will handle it here.
I would also add a check to make sure that your reference to this.list exists before calling the scrollToLocation function. Something like this.
_scrollToSection = index => {
setTimeout(() => {
if (this.list) {
this.list.scrollToLocation({
animated: true,
itemIndex: -1,
sectionIndex: index,
viewPosition: 0
});
}
}, 150);
};

Related

ApolloClient V3 infinite scroll goes up to the first item after fetchMore

thanks for being out here! I am new to React and Apollo and will try my best to explain my situation.
Right now I implemented an 'infinite' scroll to my website with ApolloClient V3. This infinite scroll can be seen here. As you try it out, you see that the whole page refreshes instead of just the data that gets added in fetchMoreResult.
I implemented it according to this YouTube video. In this video, you can see that it doesn't happen, the code stays at the bottom and showing an occasional spin.
Below I will show you my code:
I am using
const { data, error, loading, fetchMore, networkStatus } = useQuery(
GET_ALL_COUNTRIES_MAIN,
{
notifyOnNetworkStatusChange: true,
variables: {
offset: 0,
limit,
},
}
);
if (!data || !data.globaldatasortednew) return <CircularProgress />;
To pull my data. Then I set a waypoint to fetchMore like this:
return (
<div className={classes.container}>
<Grid container={true} justify="center" spacing={2}>
{data.globaldatasortednew.map((country, index) => {
return (
<React.Fragment key={country.countryid}>
{index === data.globaldatasortednew.length - 5 && (
<Waypoint
onEnter={() =>
fetchMore({
variables: {
limit: 30,
offset:
data.globaldatasortednew[
data.globaldatasortednew.length - 5
].countryid,
},
}).then((fetchMoreResult) => {
setLimit(
data.globaldatasortednew.length +
fetchMoreResult.data.globaldatasortednew.length
);
})
}
/>
)}
</Grid>
{networkStatus === 3 && <CircularProgress />}
On mobile phones, this causes the data to scroll back to the top which is quite inconvenient? How to prevent this and don't let the whole Grid re-render?
Once more, thanks, and I hope I was clear enough in describing!
For people who have the same problem, for me it was apollo cache policy issue.
Remove cache-and-network from fetchPolicy to be good ;)

ReactJS - Dynamic State

How do I dynamically create the state so I can assign 'open'/'close' based on front-end requirements?
E.g.
I have a table with rows in which I have a Fab component. That component has to set a unique ID to open/close to handle the animation. I do not know how many of these rows I will have so I can't pre-populate the state with active1, active2 etc.
Code Example (In this example I am passing 'active1', 'active2' with the object manually, but realistically that isn't a real approach I feel.
{Object.keys(characters).map((k, i) => {
<Fab
active= // how do I specify the state of 'this' instance?
direction="left"
containerStyle={{}}
style={{
backgroundColor: "#5067FF"
}}
position="topRight"
onPress={() => this.setState({ //how so I specify 'this' instances state? })}>
...redacted
I've tried to add a sample similar to my code. Let's say I am passing in 5 Users and each User should have this Fab. At this point my state is blank so I have tried implementing a for each loop into componentdidmount to populate the state, this worked, but I had two issues based on how I managed state.
A) All fabs were open, always.
B) No fabs would ever open.
This is because of my onPress not updating the correct state.
I feel like I have to be way over-complicating this issue and I am tearing up my code to get it to work, so I figured I'd come here to ask.
Let me know what other code I can share.
A way of achieving this is to keep an activeArray in state,
which you can toggle activeness of an element buy pushing or removing the element (or just its id or some property which is unique) from activeArray.
So there should be a function on onPress like:
handlePress = (k) => {
if(this.state.activeArray.indexOf(k) > -1){
// remove element
// for example:
this.setState(state => ({activeArray: state.activeArray.filter(activeElement => activeElement !== k)}))
} else {
// push element
// for example:
this.setState(state => ({activeArray: [...state.activeArray, k]}))
}
}
Then for Fab component:
<Fab
active={this.state.activeArray.indexOf(k) > -1}
direction="left"
containerStyle={{}}
style={{
backgroundColor: "#5067FF"
}}
position="topRight"
onPress={() => this.handlePress(k)}>
This line:
active={this.state.activeArray.indexOf(k) > -1}
means if k is inside activeArray then active is true.
You can set the dynamic state using JSON bracket ([]) notation.
{
Object.keys(characters).map((k, i) => {
let dynamicStateName = "active"+i //it will set the dynamicStateName as active with index. ie, active1, active2 and so on
return(
{ this.state[dynamicStateName] !== false || this.state[dynamicStateName] !== undefined ?
(<Fab
active= {dynamicStateName}// specify the state of 'this' instance here as dynamicStateName
direction="left"
containerStyle={{}}
style={{
backgroundColor: "#5067FF"
}}
position="topRight"
onPress={() => {(this.state[dynamicStateName] != undefined) ? (this.state[dynamicStateName] ? this.setState({ [dynamicStateName]: false }) : this.setState({ [dynamicStateName]: true })) : this.setState({ [dynamicStateName]: true })} } /> // it will set the state as true if undefined. It will act as toggleable
):
( <button onPress={() => this.setState({ [dynamicStateName]: true })}>OpenFabButton<button>
// initially the dynamicState will be either undefined or false. At that time the button will be show. On clicking the button it will enable the fab component
)
)
})
}
This will fix yours

Flatlist onEndReached endless loop

I am using state to store the following data.
state = {
refresh: true,
isFetching: true,
showLoadingBottom: false,
data: []
};
on componentDidMount I manually call a function _fetchData which loads data into this.state.data.
When the flatlist is scrolled to the end it fires _fetchData twice which ends up returning the same data twice ( which is another problem, why does it fire twice? ).
Once the flatlist reaches the end ie, no more data is returned from the server, it goes into an endless loop since the onEndReached continuously fires over and over again even though no new data is returned from the server and this.state.data remains the same.
This is my render code
render() {
return (
<View
style={{
flex: 1
}}>
<FlatList
refreshControl={
<RefreshControl
refreshing={this.state.refresh}
onRefresh={() => {
this.setState({
refresh: true
}, this._fetchData);
}}
title={"Pull To Refresh"}
tintColor={darkGrey}
titleColor={darkGrey}/>
}
onEndReachedThreshold={0.5}
onEndReached={() => {
this.setState({
showLoadingBottom: true
}, () => {
this._fetchData();
});
}}
showsVerticalScrollIndicator={false}
data={this.state.data}
ListFooterComponent={() => {
return (
this.state.showLoadingBottom &&
<View style={{padding: 10}}>
<ActivityIndicator size="small" color={colorAccent}/>
</View>
);
}}
renderItem={this._renderItem}
keyExtractor={(item) => item.id.toString()}
removeClippedSubviews={true}
/>
</View>
);
}
Here is my solution that can maybe be changed to suit other peoples needs:
Basically the important parts are onEndReached={this.state.moreData && this.retrieveMore}. So you can test inside your onEndReached function weather there is anymore data (In my case if we only return 1 object i know it's finished) then set state this.state.moreData to false.
<SafeAreaView style={styles.container}>
<FlatList
data={Object.values(this.state.documentData)}
// Render Items
renderItem={({ item }) => (
<ItemSelector
item={item}
onPress={() => {this.selectItem(item)}}
/>
)}
// On End Reached (Takes in a function)
onEndReached={this.state.moreData && this.retrieveMore}
// How Close To The End Of List Until Next Data Request Is Made
onEndReachedThreshold={1}
ListEmptyComponent={
<Text>No jobs to show</Text>
}
/>
</SafeAreaView>
retrieveMore = async () => {
try {
// Set State: Refreshing
this._isMounted && this.setState({ refreshing: true });
fbDb.ref('job')
.orderByKey()
.startAt(this.state.lastVisible) //Start at the last item we found
.limitToFirst(this.state.limit) //Limit queries returned per page
.once('value', snapshot => {
//check if we got a result
if(snapshot.numChildren() > 1){
.....
this._isMounted && this.setState({
documentData: newstate,
lastVisible: lastVisible,
refreshing: false, //Hide loading icon
});
} else {
this._isMounted && this.setState({
refreshing: false, //Hide loading icon
moreData: false
});
}
});
}
catch (error) {
console.log(error);
}
};
I have a similar problem. In my case it is because the ListFooterComponent.
If you render the ListFooterComponent with this pattern or equivalent
onEndReachedThreshold={x} // for any x >= 0
ListFooterComponent={() => {
if (isDataFetching) {
return <SomeComponentWithSomeHeight />
} else {
return undefined;
}
}}
It will trigger onEndReached infinitely when the user scrolls down the end of the list (or if your content is not longer than the list's visibility area).
And it is because the presence and absence of the <SomeComponentWithSomeHeight /> affects the height of the content and thus triggers the endReached re-calculation.
And following are the possible solution I can think of.
Use negative onEndReachedThreshold that is always "higher" than the height of the ListFooterComponent. But I don't like this solution because it is difficult to know the "higher" (it is relative to the FlatList's visibility area). And the negative onEndReachedThreshold may cause some issue on Android.
Implement your own loading overlay outside of the FlatList so that the loading component does not affect the content height.
Set opacity = 0 to hide the ListFooterComponent instead of returning undefined, so that it is always there and the content height does not change when it becomes visible.
ListFooterComponent={() => {
return (
<View style={{ opacity: isDataFetching ? 1 : 0 }}>
<SomeComponentWithSomeHeight />
</View>
);
}}
You are using a component that is being rendered when you are loading data, right? So your flatlist needs to rerender. You meed to make sure you only call you fetch method once even thou your end is reached multiple times.

Can rxjs control the maximum frequency of a function call?

I am making a react-native app. Since the onEndReached props in FlatList is problematic, onEndReached can be triggered more than once when the end is reached.
I have heard of rxjs that can make a button's onPress only be triggered once with some condition even user is clicking on it multiple times.
Below is the Flatlist:
<FlatList
data={paginatedList}
ListHeaderComponent={() => this.renderHeader()}
renderItem={({item, index}) => this.renderItem(item, index)}
onEndReachedThreshold={0}
onEndReached={(distanceFromEnd) => {
console.log(distanceFromEnd);
this.setState({normalListLength: normalListLength + 10})
}}
/>
I want the this.setState function to be limited to once per seconds (1000ms) . Is that I should use rxjs to do this?
So one possible solution could be to have a Subject which you can next() the new value (distanceFromEnd) into. And then you can apply any combination of operators (including debounceTime) to enforce the frequency limiting.
Keep in mind my React syntax might not be spot on
<FlatList
data={paginatedList}
ListHeaderComponent={() => this.renderHeader()}
renderItem={({item, index}) => this.renderItem(item, index)}
onEndReachedThreshold={0}
onEndReached={(distanceFromEnd) => {
console.log(distanceFromEnd);
myOnEndReachedSubject.next(distanceFromEnd);
this.setState({normalListLength: normalListLength + 10})
}}
/>
// elsewhere define subject
myOnEndReachedSubject = new Subject<number>();
// ....elsewhere in a lifecycle function
componentDidMount() {
myOnEndReachedSubject
.debounceTime(1000) // debounce for a second
.distinctUntilChanged()
.subscribe((distance) => {
// Do something with distance
// setState etc
});
}

Flatlist scroll references with getItemLayoutProp

I've got a little problem with flatlist and scroll methods.
I have flatlist with comments and if new one is added I want to scroll list to the bottom to see added comment.
Using scrollToIndex doesn't work properly, native keep showing errors due to lack of getItemLayout, and here is another problem with setting this function - every item can have different size.
scrollToEnd has some issues, sometimes it scrolls to almost bottom of the page, sometimes to headerComponent elements which are set in flatlist props.
Do you have any ideas how to make it scroll to the bottom?
To use scrollToIndex you need to use getItemLayout. There is no point in using it if you have no intention to use getItemLayout. Here is an example taken from the react-native docs:
class ScrollToExample extends Component {
getItemLayout = (data, index) => (
{ length: 50, offset: 50 * index, index }
)
scrollToIndex = () => {
let randomIndex = Math.floor(Math.random(Date.now()) *
this.props.data.length);
this.flatListRef.scrollToIndex({animated: true, index:
randomIndex});
}
scrollToItem = () => {
let randomIndex = Math.floor(Math.random(Date.now()) *
this.props.data.length);
this.flatListRef.scrollToIndex({animated: true, index: "" +
randomIndex});
}
render() {
return (
<FlatList
style={{ flex: 1 }}
ref={(ref) => { this.flatListRef = ref; }}
keyExtractor={item => item}
getItemLayout={this.getItemLayout}
initialScrollIndex={50}
initialNumToRender={2}
renderItem={({ item, index}) => (
<View style={{...style, backgroundColor: this.getColor(index)}}>
<Text>{item}</Text>
</View>
)}
{...this.props}
/>
);
}
}
https://gist.github.com/joshyhargreaves/b8eb67d24ce58a6d8bffb469f7eeaf39
Hope this helps!

Resources