What is the best implementation of pagination using Apollo hooks in a react native flatlist? - react-native

Im looking for insight on how best to implement a loadMore function in the onEndReached callback provided by flatlist while using apollo hooks! I've got it sort of working except every time i load more results the list jumps to the top since the data field of flatlist relies on incoming data from useQuery that changes every time it asks for more...
I dont know if i should be implementing offset and limit based pagination, cursor based, or some other strategy.
If anyone has tips that would be huge! thanks!

I am using Shopify storefront graphql queries to get product list, and here is how I have implemented pagination using cursor-based pagination method on FlatList. Hope you find something usable.
First, declare two variables which will be used later to check whether the Flatlist scrolled and reached on the end.
// declare these two variables
let isFlatlistScrolled = false;
let onEndReachedCalledDuringMomentum = false;
Now, create a method called handleFlatlistScroll which will be used to changed the values of the variable isFlatlistScrolled when the flatlist is scrolled.
const handleFlatlistScroll = () => {
isFlatlistScrolled = true;
};
Also declare a method to change the value of onEndReachedCalledDuringMomentum.
const onMomentumScrollBegin = () => {
onEndReachedCalledDuringMomentum = false;
}
Now, create your flatlist like this :
return (
<Layout style={{ flex: 1 }}>
<Query
query={GET_PRODUCT_LIST_BY_COLLECTION_HANDLE}
variables={{
handle: props.route.params.handle,
cursor: null,
}}>
{({
loading,
error,
data,
fetchMore,
networkStatus,
refetch,
stopPolling,
}) => {
if (loading) {
return <ProductListPlaceholder />;
}
if (data && data.collectionByHandle?.products?.edges?.length > 0) {
stopPolling();
return (
<FlatList
data={data.collectionByHandle.products.edges}
keyExtractor={(item, index) => item.node.id}
renderItem={renderProductsItem}
initialNumToRender={20}
onScroll={handleFlatlistScroll}
onEndReached={() => {
if (
!onEndReachedCalledDuringMomentum &&
isFlatlistScrolled &&
!isLoadingMoreProducts &&
!loading &&
data.collectionByHandle?.products?.pageInfo?.hasNextPage
) {
onEndReachedCalledDuringMomentum = true;
setLoadingMoreProductsStatus(true);
// your loadmore function to fetch more products
}
}}
onEndReachedThreshold={Platform.OS === 'ios' ? 0 : 0.1}
onMomentumScrollBegin={onMomentumScrollBegin}
// ... your other flatlist props
/>
);
}
return <EmptyProductList />;
}}
</Query>
</Layout>
)
As you can see in above code, load more function only called when flatlist is properly scrolled at the end.

Related

Efficient way to scroll to certain index in FlatList with variable item size

I'm having a trouble adding scroll/jump to certain index functionality on FlatList in react-native. My FlatList items are vary in size (height) which makes me unable to implement getItemLayout since this requires me to have prior knowledge about the FlatList item size, therefore I cannot use scrollToIndex (which requires getItemLayout to be implemented).
My solution was to get each item's size when rendered by using onLayout and map them with their index. I can then use each item size to get their offset and use scrollToOffset to jump to the given item (by using scrollToItem function in the code below). The issue here is that I am not able to jump to certain item until that item has been rendered.
My temporary solution for that is by tweaking initialNumberToRender close to the number of data and set the windowSize props as high as possible so that all of the items will be rendered (even though the user doesn't scroll down).
getOffsetByIndex = (index) => {
let offset = 0;
for (let i = 0; i < index; i++) {
const elementLayout = this.layoutMap[index];
if (elementLayout && elementLayout.height) {
offset += this.layoutMap[i].height;
}
}
return offset;
};
scrollToItem = (index) => {
const offset = this.getOffsetByIndex(index);
this.flatListRef.scrollToOffset(offset);
};
addToLayoutMap = (layout, index) => {
this.layoutMap[index] = layout;
};
render(){
return(
<FlatList
ref={this.flatListRef}
data={this.state.data}
renderItem={() => <View onLayout={this.addToLayoutMap}> <SomeComponent/> </View>}
initialNumToRender={this.state.data.length / 5}
windowSize={this.state.data.length}
/>
);
}
This solution works with small number of data, but when the data is large (containing ~300 of rows), it will take long time to be rendered get all the item size, preventing the user to jump to the last item directly for example.
Is there any efficient way to do it?
Also, rendering all the rows is so memory consumptive and negates the benefit of using FlatList.
You can dynamically split your data according to scroll direction. If scroll goes up prepend data to your state and same for opposite direction. Then use onScrollToIndexFailed like this :
<FlatList
ref={this.flatListRef}
data={this.state.data}
renderItem={() => <View> <SomeComponent /> </View>}
initialNumToRender={this.state.data.length / 5}
onEndReached={(e) => {
// Append data
}}
onScroll={(e) => {
if (e.nativeEvent.contentOffset.y == 0) {
// Prepend data
}
}}
onScrollToIndexFailed={(error) => {
this.flatListRef.scrollToOffset({ offset: error.averageItemLength * error.index, animated: true });
setTimeout(() => {
if (this.state.data.length !== 0 && this.flatListRef !== null) {
this.flatListRef.scrollToIndex({ index: error.index, animated: true });
}
}, 100);
}}
/>
You can workaround this issue. This worked for me and took me a lot of time to get that work :))

Need assistance fixing maximum update depth exceeded issue

I'm trying to write a hacky fix to ScrollableTabView since it isn't playing nice with the function that triggers when there's a tab switch. When I replace the setState with console.log I see that it only triggers once with every tab switch so it's not looping infinitely like the error is complaining.
Parent container
state = {
headerName: 'Loading',
}
setHeader = (header) => {
this.setState({'headerName': header})
}
render () {
return (
<ScrollableTabView
renderTabBar={() => <BottomTabBar setHeader={this.setHeader} headerNames={['A','B','C']} />}
>
)
}
BottomTabBar
render() {
this.props.setHeader(this.props.headerNames[this.props.activeTab])
...
}

How to update a single item in FlatList in React Native?

Attention: I have posted an answer down there, personally I think it's the best solution so far. Even though it's not the highest rated answer, but based on the result I'm getting, it is very efficient.
---------------------------------------------Original Question-------------------------------------------------------
Suppose I am writing a Twitter clone, but much simpler. I put each item in FlatList and render them.
To "like" a post, I press the "like" button on the post and the "like" button turns red, I press it again, it turns gray.
This is what I have so far: I store all the loaded posts in this.state, each post has a property called "liked", which is boolean, indicating whether this user has liked this post or not, when user presses "like", I go to state.posts and update the liked property of that post, and then use this.setState to update posts like so:
// 1. FlatList
<FlatList
...
data={this.state.posts}
renderItem={this.renderPost}
...
/>
// 2. renderPost
renderPost({ item, index }) {
return (
<View style={someStyle}>
... // display other properties of the post
// Then display the "like" button
<Icon
name='favorite'
size={25}
color={item.liked ? 'red' : 'gray'}
containerStyle={someStyle}
iconStyle={someStyle}
onPress={() => this.onLikePost({ item, index })}
/>
...
</View>
);
}
// 3. onLikePost
likePost({ item, index }) {
let { posts } = this.state;
let targetPost = posts[index];
// Flip the 'liked' property of the targetPost
targetPost.liked = !targetPost.liked;
// Then update targetPost in 'posts'
posts[index] = targetPost;
// Then reset the 'state.posts' property
this.setState({ posts });
}
This approach works, however, it is too slow. The color of the "like" button flips as I press it, but it usually takes about 1 second before the color changes. What I want is that the color would flip almost at the same time when I press it.
I do know why this would happen, I should probably not use this.setState, because when I do that, the posts state changed, and all posts get re-rendered, but what other approach can I try?
You can set extraData in FlatList:
<FlatList
...
extraData={this.state}
data={this.state.posts}
renderItem={this.renderPost}
...
/>
When state.posts or state.posts's item change, FlatList will re-render.
From FlatList#extradata:
A marker property for telling the list to re-render (since it implements PureComponent). If any of your renderItem, Header, Footer, etc. functions depend on anything outside of the data prop, stick it here and treat it immutably.
Update:
Functional component implementation:
export default function() {
// list of your data
const [list, setList] = React.useState([])
const [extraData, setExtraData] = React.useState(new Date())
// some update on the item of list[idx]
const someAction = (idx)=>{
list[idx].show = 1
setList(list)
setExtraData(new Date())
}
return (
<FlatList
// ...
data={list}
extraData={extraData}
/>
)
}
After updating list, I use setExtraData(new Date()) to tell the FlatList to re-render. Because the new time is different from the previous.
Don't get me wrong, #ShubhnikSingh's answer did help, but I retracted it because I found a better solution to this question, long time ago, and finally I remembered to post it here.
Suppose my post item contains these properties:
{
postId: "-L84e-aHwBedm1FHhcqv",
date: 1525566855,
message: "My Post",
uid: "52YgRFw4jWhYL5ulK11slBv7e583",
liked: false,
likeCount: 0,
commentCount: 0
}
Where liked represents whether the user viewing this post has liked this post, which will determine the color of the "like" button (by default, it's gray, but red if liked == true)
Here are the steps to recreate my solution: make "Post" a Component and render it in a FlatList. You can use React's PureComponent if you don't have any props that you pass to your Post such as an array or object that can be deceptively not shallow equal. If you don't know what that means, just use a regular Component and override shouldComponentUpdate as we do below.
class Post extends Component {
// This determines whether a rendered post should get updated
// Look at the states here, what could be changing as time goes by?
// Only 2 properties: "liked" and "likeCount", if the person seeing
// this post ever presses the "like" button
// This assumes that, unlike Twitter, updates do not come from other
// instances of the application in real time.
shouldComponentUpdate(nextProps, nextState) {
const { liked, likeCount } = nextProps
const { liked: oldLiked, likeCount: oldLikeCount } = this.props
// If "liked" or "likeCount" is different, then update
return liked !== oldLiked || likeCount !== oldLikeCount
}
render() {
return (
<View>
{/* ...render other properties */}
<TouchableOpacity
onPress={() => this.props.onPressLike(this.props.postId)}
>
<Icon name="heart" color={this.props.liked ? 'gray' : 'red'} />
</TouchableOpacity>
</View>
)
}
}
Then, create a PostList component that will be in charge of handling the logic for loading posts and handling like interactions:
class PostList extends Component {
/**
* As you can see, we are not storing "posts" as an array. Instead,
* we make it a JSON object. This allows us to access a post more concisely
* than if we stores posts as an array. For example:
*
* this.state.posts as an array
* findPost(postId) {
* return this.state.posts.find(post => post.id === postId)
* }
* findPost(postId) {
* return this.state.posts[postId]
* }
* a specific post by its "postId", you won't have to iterate
* through the whole array, you can just call "posts[postId]"
* to access it immediately:
* "posts": {
* "<post_id_1>": { "message": "", "uid": "", ... },
* "<post_id_2>": { "message": "", "uid": "", ... },
* "<post_id_3>": { "message": "", "uid": "", ... }
* }
* FlatList wants an array for its data property rather than an object,
* so we need to pass data={Object.values(this.state.posts)} rather than
* just data={this.state.posts} as one might expect.
*/
state = {
posts: {}
// Other states
}
renderItem = ({ item }) => {
const { date, message, uid, postId, other, props, here } = item
return (
<Post
date={date}
message={message}
uid={uid}
onPressLike={this.handleLikePost}
/>
)
}
handleLikePost = postId => {
let post = this.state.posts[postId]
const { liked, likeCount } = post
const newPost = {
...post,
liked: !liked,
likeCount: liked ? likeCount - 1 : likeCount + 1
}
this.setState({
posts: {
...this.state.posts,
[postId]: newPost
}
})
}
render() {
return (
<View style={{ flex: 1 }}>
<FlatList
data={Object.values(this.state.posts)}
renderItem={this.renderItem}
keyExtractor={({ item }) => item.postId}
/>
</View>
)
}
}
In summary:
1) Write a custom component (Post) for rendering each item in "FlatList"
2) Override the "shouldComponentUpdate" of the custom component (Post) function to tell the component when to update
Handle the "state of likes" in a parent component (PostList) and pass data down to each child
If you are testing on android than try turning off the developer mode. Or are you hitting some API and updating the post on the server and updating the like button in UI corresponding to the server response? If that is the case do tell me, I too have encountered this and I solved it. Also I have commented the second last line in your code which isn't needed.
// 1. FlatList
<FlatList
...
data={this.state.posts}
renderItem={this.renderPost}
...
/>
// 2. renderPost
renderPost({ item, index }) {
return (
<View style={someStyle}>
... // display other properties of the post
// Then display the "like" button
<Icon
name='favorite'
size={25}
color={item.liked ? 'red' : 'gray'}
containerStyle={someStyle}
iconStyle={someStyle}
onPress={() => this.onLikePost({ item, index })}
/>
...
</View>
);
}
// 3. onLikePost
likePost({ item, index }) {
let { posts } = this.state;
let targetPost = posts[index];
// Flip the 'liked' property of the targetPost
targetPost.liked = !targetPost.liked;
// Then update targetPost in 'posts'
// You probably don't need the following line.
// posts[index] = targetPost;
// Then reset the 'state.posts' property
this.setState({ posts });
}

React native navigator passing parent component to child

I don't know how to pass a reference to the TripList instance below to the AddTrip component. I need to do something like that to signal to TripList to refresh the data after adding a new trip.
In my render() method, inside <Navigator> I have:
if (route.index === 1) {
return <TripList
title={route.title}
onForward={ () => {
navigator.push({
title: 'Add New Trip',
index: 2,
});
}}
onBack={() => {
if (route.index > 0) {
navigator.pop();
}
}}
/>
} else {
return <AddTrip
styles={tripStyles}
title={route.title}
onBack={() => { navigator.pop(); }}
/>
}
However, when I call onBack() in AddTrip, after adding a trip, I want to call refresh() on TripList so the new trip is displayed. How best can I structure things to do that? I'm guessing I need to pass TripList somehow to AddTrip and then I can call refresh() there easily right before calling onBack().
This is not how React works. You don't pass instances of a component around, rather you pass the data to your component via props. And your component AddTrip should receive another props which is a function to call when adding a trip.
Let me illustrate this with a code example, this is not how your code should be in the end, but it'll illustrate how to contain the data outside of your components.
// Placed at the top of the file, not in a class or function.
let allTrips = [];
// Your navigator code.
if (route.index === 1) {
return <TripList
trips={allTrips}
title={route.title}
onForward={ () => {
navigator.push({
title: 'Add New Trip',
index: 2,
});
}}
onBack={() => {
if (route.index > 0) {
navigator.pop();
}
}} />
} else {
return <AddTrip
styles={tripStyles}
title={route.title}
onAdd={(tripData) => {
allTrips = [...allTrips, tripData];
}}
onBack={() => { navigator.pop(); }} />
}
As you can see, the logic about adding and finding the trips comes from the parent component, which is the navigator in this case. You will also note that we are reconstructing the content of allTrips, this is important as React is based on the concept of immutability.
You must have heard of Redux which is a system allowing all your components to discuss with a global store from which you fetch and save all your application state. It's a bit more complex that's why I did not use it as an example it.
I'll almost forget the most important! You will not need to signal to to your component that it needs refreshing, the magic of React should take care of it by itself!

React Native List View - Prepend items without rendering whole list

I am trying to implement an infinite scroll with pull to refresh functionality.
I get the new data, append it via concat and it renders fine. The problem is that it renders the whole list with it. If i have >500 items, it becomes a nightmare.
onRefresh() {
var posts = this.state.posts;
var firstPost = posts[0].m._id;
server.getStream('', firstPost, 4000)
.then(res => {
posts = res.concat(posts);
this.setState({
dataSource: this.ds.cloneWithRows(posts),
posts
});
this.swipeRefreshLayout && this.swipeRefreshLayout.finishRefresh();
})
}
Is there a way to tell RN to render only new rows? Some sort of key prop maybe?
Thanks
UPDATE
The rowHasChanged comparator in DatSource does not fire. I think that the way I've structured my component may have something to do with it.
renderRow(post) {
if(post === 'loader') {
return (
<ProgressBarAndroid
styleAttr="Large"
style={styles.spinnerBottom}/>
)
}
let hasLoader = post.m._id === lastPostId;
let loader = hasLoader ?
<ProgressBarAndroid
styleAttr="Large"
style={styles.spinnerBottom}/> : null;
return (
<View>
<Post
post={post}
isLoadingTop={isLoadingTop}/>
{loader}
</View>
)
}
render() {
var stream = <ListView
dataSource={this.state.dataSource}
renderRow={this.renderRow.bind(this)}
onEndReached={this.onEndReached.bind(this)}
onEndReachedThreshold={1}
pageSize={15} />
return (
<View style={styles.mainContainer}>
<View style={styles.header}>
<Text style={styles.headerText}>Header</Text>
</View>
<SwipeRefreshLayoutAndroid
ref={(c) => {
this.swipeRefreshLayout = c;
}}
onRefresh={this.onRefresh.bind(this)}>
{stream}
</SwipeRefreshLayoutAndroid>
</View>
);
Any ideas?
You need to implement the DataSource.rowHasChanged comparator in a way that it renders false for rows whose data has not changed. In cases where the existing list items do not change, you can use a simple reference equality check:
let dataSource = new ListView.DataSource({
rowHasChanged: (prev, next) => prev !== next
});
In cases where the rows you don't want to change are not the same objects as used on the previous render pass, you need to implement a more specific comparator function.