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?
Since FlatList is a PureComponent , it assumes that the data is an immutable object and that any change in data is done by setting a new data source object in the state object. Alternatively, use the extraData={this. state. isChanged} prop to re-render a FlatList component by updating the value of isChanged property.
Typically you'll want an edit callback handler that takes the index or id of the data element to edit (in order to toggle some edit view), the edit mode view (i.e. some input), and a save edit handler to update that element in your data in state.
If you are using a FlatList in React Native, there's a chance you will want to use the scrollToIndex function to scroll to a certain item's index. Unfortunately, this function is a lot easier to use with fixed item heights where it is easy to implement the required getItemLayout function.
flatlist-simple By passing extraData={selectedId} to FlatList we make sure FlatList itself will re-render when the state changes. Without setting this prop, FlatList would not know it needs to re-render any items because it is a PureComponent and the prop comparison will not show any changes.
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.
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 love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With