Prevent FlatList from re-rendering entire list when state changes

Viewed 2070

I have an array of posts in a redux reducer that I update via the onEndReached prop of my FlatList. The issue is, every time the state changes, the entire list is re-rendered rather than the affected post item.

// Posts Feed Screen

const posts = useSelector(state => state.posts)

const renderItem = useCallback(
    ({ item }) => (
        <Post post={item} />
    ),
    []
)
const keyExtractor = useCallback(
    (item, index) => `${item.id}${index}`,
    []
)

return (
    <FlatList
        ref={ref}
        data={posts}
        keyExtractor={keyExtractor}
        renderItem={renderItem}
        onEndReached={onEndReached}
        onEndReachedThreshold={0.6}
        showsVerticalScrollIndicator={false}
        removeClippedSubviews
    />
)



// Post Component

const Post = ({ post }) => {
    return (
        <View style={styles.post}>
            <PostHeader />
            <View style={styles.postBody}>
                <PostContent />
                <PostFooter />
            </View>
        </View>
    )
}

const isEqual = (prevProps, nextProps) => {
    return prevProps.post.id === nextProps.post.id
}

export default React.memo(Post, isEqual)

As you can see, I'm utilizing useCallback on renderItem function, also using React.memo on the export of my Post Component - however, the FlatList still renders every post item on state change.

What am I missing here?

3 Answers

I am going to post my comments as an answer because ultimately that is how the problem was solved:

This is actually simpler than it seems. What happens is that when you pass "reference types" as props everytime a new reference is created the component will re-render.

So in your reducer that updates the "posts" you should make sure you are not creating a new list every time but rather updating only properties of a single post within the list.

Avoid spread operator for example. Also the post component should not take a post as a prop but actually an Id of the post and find it itself using the useSelector which in turn should return values of the properties.

So for example it is better to have a useSelector per property being displayed than a single useSelector for the whole post. The reason is that the react rendering engine compares using "==" to find differences and decide how to render.

And two objects can be exactly the same compared property by property, but yet return as different because they are different instances.

The key here is to only update the properties that need updating inside the reducers and at the same time make sure you return value types in your useSelectors.

I'm wondering how you used the keyExtractor function. In react conciliation algorithm, key plays an important role as you can see from here. If all your post components are rendered every time, please consider the following situations.

use of keyExtractor

If key is unique in the siblings and remains same across rendering, React won't redraw the post with the same key as previous posts(in the previous rendering). If key is not unique, unexpected result would happen, and if key is changing every render, all posts would re-rendered.

In your code, keyExtractor combines the id and index. What if post order changed? key would be different even for the same post if its order is not same as before. This might be a problem. If your post id isn't changed and unique among posts, you can use id as key. That's the common way to find keys.

conditional rendering

I don't know your FlatList component implementation details. If this component draws the posts in some cases and doesn't in other cases, React would re-render all posts even though they have unique and not-changed keys.

Other reasons may exist, but I hope these possible cases would help you. Below are my abstract code.

FlatList:
const { 
    data,
    keyExtractor,
    renderItem: Component,
    ...
} = props;

return (
    <>
        {data && data.map((post, index) => (
            <Component key={keyExtractor(post)} item={post} />
        ))}
    </>
)

// Posts Feed Screen
const renderItem = useCallback(
    ({ item }) => (
      <Post post={item} />
    ),
    []
)

const keyExtractor = useCallback(
    (item, index) => `${item.id}`,
    []
  )

what about this:

const [posts, setPosts] = useState([]);

const onEndReached = async () => {
    await MyApi().getPosts()
    .then(newPosts => {
        setPosts([...posts, ...newPosts]);
    })
}
Related