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.
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 can I store the recipe name from this api call into a state variable or some other variable to be used further down in the render
{
this.props.recipeList.recipeList.filter((recipe) => recipe.recipeName === search).map(recipe => {
return <Recipe
name={recipe.recipeName}
numberOfServings={recipe.numberOfServings}
key={'recipe-' + recipe.recipeId}
/>
})
}
As this is performed within render, avoid modifying the component state.
I would recommend declaring a variable at the top of your render method and then assigning it in your map call. This, of course, assumes that filter will only ever return a single result.
Something like:
render() {
let name;
....
{
this.props.recipeList.recipeList.filter((recipe) => recipe.recipeName === search).map(recipe => {
name = recipe.recipeName; // this bit
return <Recipe
name={recipe.recipeName}
numberOfServings={recipe.numberOfServings}
key={'recipe-' + recipe.recipeId}
/>
})
}
....
I've been working to implement MobX into one of my classes, and I believe I'm close to have it working but I'd really appreciate if someone can point out where I'm going wrong here.
Essentially when the refreshJobs() function runs, I'd like the render() function to execute again. From my understanding, if I updated the observable object jobs, the computed functions (renderSubmittedJobs(), renderInProgressJobs()) would run again producing new values, then the render function would run again since those values have been updated.
However, what happens with this code is that it updates this.jobs (wrapped in an action), but neither of the computed functions execute - and I believe that is why render isn't ran again either.
Does anyone know what might be causing this issue? I really appreciate any direction with this.
#observer
export default class Jobs extends React.Component<ScreenProps<>> {
#observable jobs = {};
#computed get renderInProgressJobs() {
inProgressJobs = [];
for (key in this.jobs) {
if (jobs[key].status === "in progress") {
inProgressJobs.push(this.jobs[key]);
}
}
return this.renderJobComponents(inProgressJobs);
}
#computed get renderSubmittedJobs() {
submittedJobs = [];
for (key in this.jobs) {
console.log(key)
if (this.jobs[key].status !== "in progress") {
submittedJobs.push(this.jobs[key]);
}
}
return this.renderJobComponents(submittedJobs);
}
renderJobComponents(jobList: Array) {
return jobList.map((jobInfo, key) => {
return (
...
);
});
}
#observer
async refreshJobs() {
jobs = await grabClientJobs(refresh=true);
await runInAction("Updating Jobs", async () => {
this.jobs = jobs;
});
}
#observer
async componentWillMount() {
jobs = await grabClientJobs();
runInAction("Updating Jobs", async () => {
this.jobs = jobs;
});
}
#observer
render(): React.Node {
console.log('in jobs now');
return <BaseContainer title="Jobs" navigation={this.props.navigation} scrollable refresh={this.refreshJobs}>
<Tabs renderTabBar={()=> <ScrollableTab />} tabBarUnderlineStyle={style.secondaryBackground}>
<Tab heading="In Progress" textStyle={style.tabTextStyle} activeTextStyle={style.activeTabTextStyle}>
{ this.renderInProgressJobs }
<Button full style={[style.secondaryBackground, style.newJob]}>
<Text>CREATE NEW JOB</Text>
</Button>
</Tab>
<Tab heading="Submitted" textStyle={style.tabTextStyle} activeTextStyle={style.activeTabTextStyle}>
{ this.renderSubmittedJobs }
</Tab>
</Tabs>
</BaseContainer>;
}
}
few mistakes here:
you can't re-assign another value to the observable variable, it'll destroy the observable. You need to mutate the observable. For example, you can directly assign values to existing properties or use extendObservable for assigning new properties to observable object.
If you use MobX < 4, adding new properties the observable object will not trigger changes because the properties are set when the observable object was created. extendObservable may work but it's also limited. Use observable Map instead.
#observer should be used for component class (or SFC), not member functions inside of the class
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 });
}