facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
119.53k stars 24.37k forks source link

ListFooterComponent prop raises performance warning if it's dynamic. #32924

Closed alielmajdaoui closed 2 years ago

alielmajdaoui commented 2 years ago

Description

After few hours of debugging the FlatList/VirtualizedList performance warning:

VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc

I have found that ListFooterComponent is causing the issue if we pass a dynamic value to it (e.g: ListFooterComponent={isLoading && <Footer />}). If we pass a static value (e.g: ListFooterComponent={<Footer />}, the warning will never appear.

Version

0.66.0

Output of npx react-native info

System: OS: macOS 11.6 CPU: Intel(R) Core i7 CPU @ 2.50GHz Memory: 16.00 GB Shell: 3.2.57 - /bin/bash Binaries: Node: 14.17.6 - ~/.nvm/versions/node/v14.17.6/bin/node Yarn: 1.22.11 - /usr/local/bin/yarn npm: 6.14.15 - ~/.nvm/versions/node/v14.17.6/bin/npm Watchman: 2021.09.06.00 - /usr/local/bin/watchman Managers: CocoaPods: 1.11.2 - /usr/local/bin/pod SDKs: iOS SDK: Platforms: DriverKit 21.0.1, iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0 IDEs: Xcode: 13.1/13A1030d - /usr/bin/xcodebuild npmPackages: @react-native-community/cli: Not Found react: 17.0.2 => 17.0.2 react-native: 0.66.0 => 0.66.0 react-native-macos: Not Found npmGlobalPackages: react-native: Not Found

Steps to reproduce

  1. Create a React Native app.
  2. Use the code example in the new React Native app.
  3. Build and launch the app.
  4. Keep scrolling until no more data can be loaded.
  5. Try to scroll up and the performance warning will appear right away, even that the list is not large (20 items).

ListFooterComponent

Snack, code example, screenshot, or link to a repository

// App.tsx
import React from 'react';
import { SafeAreaView } from 'react-native';
import List from './List';

const App = () => {
    return (
        <SafeAreaView style={{ flex: 1 }}>
            <List />
        </SafeAreaView>
    );
};

export default App;
// List.tsx

import React, { memo, useCallback, useEffect, useState } from 'react';
import {
    ActivityIndicator,
    ListRenderItem,
    StyleSheet,
    Text,
    View,
    VirtualizedList,
} from 'react-native';

const data = [
    { id: 1, name: 'Abarth' },
    { id: 2, name: 'Alfa Romeo' },
    { id: 3, name: 'Aston Martin' },
    { id: 4, name: 'Audi' },
    { id: 5, name: 'Audi Sport' },
    { id: 6, name: 'BAIC Motor' },
    { id: 7, name: 'BeiBen' },
    { id: 8, name: 'Bentley' },
    { id: 9, name: 'Berkeley' },
    { id: 10, name: 'BharatBenz' },
    { id: 11, name: 'Bizzarrini' },
    { id: 12, name: 'BMW' },
    { id: 13, name: 'Brabus' },
    { id: 14, name: 'Bugatti' },
    { id: 15, name: 'Cadillac' },
    { id: 16, name: 'Chevrolet' },
    { id: 17, name: 'Chrysler' },
    { id: 18, name: 'Corre La Licorne' },
    { id: 19, name: 'Dacia' },
    { id: 20, name: 'Daewoo' },
];

const getData = (from: number) => {
    if (from > data.length) {
        return [];
    }

    return data.filter((_, i) => i >= from && i < from + 5);
};

type ItemProps = {
    name: string;
};

const ListItem = memo(({ name }: ItemProps) => {
    return (
        <View style={styles.itemContainer}>
            <Text style={styles.itemText}>{name}</Text>
        </View>
    );
});

const Footer = () => {
    return (
        <View style={styles.footerContainer}>
            <ActivityIndicator size={'large'} />
        </View>
    );
};

const List = () => {
    const [isLoading, setIsLoading] = useState(false);
    const [currentOffset, setCurrentOffset] = useState(0);
    const [items, setItems] = useState<ItemProps[]>([]);

    const getItemCount = useCallback((_data: ItemProps[]) => {
        return _data.length;
    }, []);

    const getItem = useCallback((_data: ItemProps[], index: number) => {
        return _data[index];
    }, []);

    const extractListKeys = useCallback(item => {
        return `${item.id}`;
    }, []);

    const renderItem: ListRenderItem<ItemProps> = useCallback(({ item }) => {
        return <ListItem name={item.name} />;
    }, []);

    const loadMoreData = useCallback(async () => {
        if (isLoading) {
            return;
        }

        setIsLoading(true);

        await new Promise(resolve => setTimeout(resolve, 1500));

        const _data = getData(currentOffset);

        if (_data.length > 0) {
            setItems(prevItems => [...prevItems, ..._data]);
        }

        setIsLoading(false);
        setCurrentOffset(prevOffset => prevOffset + 5);
    }, [currentOffset, isLoading]);

    useEffect(() => {
        loadMoreData();

        // I don't want to add loadMoreData to the useEffect dependencies
        // for the sake of this example.
        // Adding it will create an infinite call to loadMoreData since it
        // has dependencies (isLoading and currentOffset) that change a lot.
    }, []);

    return (
        <View style={styles.screen}>
            <View style={styles.topView}>
                <Text style={styles.topViewText}>
                    {`currentOffset: ${currentOffset}, isLoading: ${
                        isLoading ? 'Yes' : 'No'
                    }`}
                </Text>
            </View>

            <VirtualizedList
                data={items}
                initialNumToRender={4}
                style={styles.list}
                renderItem={renderItem}
                getItem={getItem}
                getItemCount={getItemCount}
                keyExtractor={extractListKeys}
                ListFooterComponent={isLoading && <Footer />}
                onEndReached={loadMoreData}
            />
        </View>
    );
};

const styles = StyleSheet.create({
    screen: {
        flex: 1,
    },
    list: {
        flex: 1,
    },
    itemContainer: {
        height: 230,
        justifyContent: 'center',
        alignItems: 'center',
        borderBottomWidth: 1,
    },
    itemText: {
        fontSize: 20,
    },
    topView: {
        padding: 14,
        backgroundColor: '#999999',
    },
    topViewText: {
        color: '#000000',
        fontSize: 18,
    },
    footerContainer: {
        alignItems: 'center',
        padding: 20,
    },
});

export default List;
darkrideroffate commented 2 years ago

Is it possible if you could show me how you replaced the isLoading logic ?

alielmajdaoui commented 2 years ago

Is it possible if you could show me how you replaced the isLoading logic ?

Please take a look at loadMoreData function.

Chandra-Panta-Chhetri commented 2 years ago

I am also facing the same issue with react-native version 0.64.3.

github-actions[bot] commented 2 years ago

This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days.

github-actions[bot] commented 2 years ago

This issue was closed because it has been stalled for 7 days with no activity.