4

I have a FlatList that receives (immutable) data of max. 50 elements and it renders in each list item Svg using react-native-svg.

Parts of the graphics are wrapped with a Pressable component for selecting the element.

Now the problem is, that I can't select any of the elements, until the FlatList went through all 50 items.

What I don't get is, that the offscreen items aren't even rendered, it's just the containers. Once it's all rendered, I can click the elements, the ripple effect shows and the event is fired.

Specs:

  • Expo @ 46.0.0
  • React Native @ 0.69.6
  • React @ 18.0.0
  • Running with android via expo start --no-dev --minify then open in Expo Go

Reproduction:

import React, { useEffect, useState } from 'react'
import { FlatList } from 'react-native'
import { Foo } from '/path/to/Foo'
import { Bar } from '/path/to/Bar'

export const Overview = props => {
  const [data, setData] = useState(null)
  
  // 1. fetching data

  useEffect(() => {
    // load data from api
    const loaded = [{ id: 0, type: 'foo' }, { id: 1, type: 'bar' }] // make a list of ~50 here
    setData(loaded)
  }, [])

  if (!data?.length) {
    return null
  }

  // 2. render list item
  const onPressed = () => console.debug('pressed')

  const renderListItem = ({ index, item }) => {
    if (item.type === 'foo') {
      return (<Foo key={`foo-${index}`} onPressed={onPressed} />)
    } 


    if (item.type === 'bar') {
      return (<Foo key={`bar-${index}`} onPressed={onPressed} />)
    }
  
    return null
  }

  // at this point data exists but will not be changed anymore
  // so theoretically there should be no re-render
  return (
    <FlatList
       data={data}
       renderItem={renderListItem}
       inverted={true}
       decelerationRate="fast"
       disableIntervalMomentum={true}
       removeClippedSubviews={true}
       persistentScrollbar={true}
       keyExtractor={flatListKeyExtractor}
       initialNumToRender={10}
       maxToRenderPerBatch={10}
       updateCellsBatchingPeriod={100}
       getItemLayout={flatListGetItemLayout}
     />
    )
  }
}


// optimized functions
const flatListKeyExtractor = (item) => item.id
const flatListGetItemLayout = (data, index) => {
  const entry = data[index]
  const length = entry && ['foo', 'bar'].includes(entry.type)
    ? 110
    : 59
  return { length, offset: length * index, index }
}

Svg component, only Foo is shown, since Bar is structurally similar and the issue affects both:

import React from 'react'
import Svg, { G, Circle } from 'react-native-svg'

const radius = 25
const size = radius * 2

// this is a very simplified example, 
// rendering a pressable circle
const FooSvg = props => {
  return (
    <Pressable
      android_ripple={rippleConfig}
      pressRetentionOffset={0}
      hitSlop={0}
      onPress={props.onPress}
    >
      <Svg
        style={props.style}
        width={size}
        height={size}
        viewBox={`0 0 ${radius * 2} ${radius * 2}`}
      >
        <G>
          <Circle
            cx='50%'
            cy='50%'
            stroke='black'
            strokeWidth='2'
            r={radius}
            fill='red'
          />
        </G>
      </Svg>
    </Pressable>
  )
}

const rippleConfig = {
  radius: 50,
  borderless: true,
  color: '#00ff00'
}

// pure component
export const Foo = React.memo(FooSvg)

The rendering performance itself is quite good, however I can't understand, why I need to wait up to two seconds, until I can press the circles, allthough they have already been rendered.

Any help is greatly appreciated.

Edit

When scrolling the list very fast, I get:

 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. {"contentLength": 4740, "dt": 4156, "prevDt": 5142}

However, the Components are already memoized (PureComponent) and not very complex. There must be another issue.

Hardware

I cross tested with an iPad and there is none if the issues described. It seems to only occur on Android.

Jankapunkt
  • 8,128
  • 4
  • 30
  • 59
  • Could you provide this code on the Expo Snack? – Vasyl Nahuliak Jan 09 '23 at 10:12
  • Hi, I have no expo account but maybe you can simply copy-paste it? The `Bar` component is similar to `Foo` it should easily work together. However, I have not tested in the browser and the targeted platform is Android. – Jankapunkt Jan 09 '23 at 13:30
  • Expo snack https://snack.expo.dev/ is not only for webapp, you can also run android in browser – Vasyl Nahuliak Jan 09 '23 at 13:43
  • @Vasyl I am currently very occupied, best I can do is put some bounty on this one – Jankapunkt Jan 10 '23 at 15:15
  • Hi, @Jankapunkt I copied your code to myself. *Foo props don't have `onPressed` prop I change `console.debug` to `alert`It shows instantly. Did you check it? *I rendered 100 Foo items and did not see You have a large list that is slow to update error. – Ayberk Anıl Atsız Jan 11 '23 at 11:29
  • @Ayberk did you execute the code on Android? You can't compare the performance with the one in the browser. I added more detailed specs to the questions. – Jankapunkt Jan 11 '23 at 13:41
  • I tested it on a Real iPad device. – Ayberk Anıl Atsız Jan 11 '23 at 13:52
  • @Ayberk thanks for pointing at this, I cross tested with an iPad from ~2016 and there is no such issue at all. It seems to be only on lower-end hardware Android devices. – Jankapunkt Jan 12 '23 at 06:29
  • @Jankapunkt I have the same issue, did you figure it out how to solve this? – pors Mar 30 '23 at 11:13
  • 1
    @pors unfortunately no! I was close to hack around in the VirtualList base class but had no time due to a release date. If you want we can connect and find a solution, it's still important for our next release to tackle this issue! – Jankapunkt Mar 30 '23 at 18:15
  • @Jankapunkt I sent you an email – pors Mar 31 '23 at 14:39

1 Answers1

1

Please ignore grammatical mistakes.

This is the issue with FlatList. Flat list is not good for rendering a larger list at one like contact list. Flatlist is only good for getting data from API in church's like Facebook do. get 10 element from API and. then in the next call get 10 more.

To render. a larger number of items like contact list (more than 1000) or something like this please use https://bolan9999.github.io/react-native-largelist/#/en/

import React, {useRef, useState} from 'react';
import {
  Image,
  StyleSheet,
  Text,
  TextInput,
  TouchableOpacity,
  View,
} from 'react-native';
import {LargeList} from 'react-native-largelist-v3';
import Modal from 'react-native-modal';
import {widthPercentageToDP as wp} from 'react-native-responsive-screen';
import FontAwesome from 'react-native-vector-icons/FontAwesome';
import fonts from '../constants/fonts';
import {moderateScale} from '../constants/scaling';
import colors from '../constants/theme';
import countries from '../Data/larger_countries.json';

const CountrySelectionModal = ({visible, setDefaultCountry, setVisible}) => {
  const pressable = useRef(true);
  const [country_data, setCountryData] = useState(countries);
  const [search_text, setSearchText] = useState('');

  const onScrollStart = () => {
    if (pressable.current) {
      pressable.current = false;
    }
  };

  const onScrollEnd = () => {
    if (!pressable.current) {
      setTimeout(() => {
        pressable.current = true;
      }, 100);
    }
  };

  const _renderHeader = () => {
    return (
      <View style={styles.headermainView}>
        <View style={styles.headerTextBg}>
          <Text style={styles.headerTitle}>Select your country</Text>
        </View>
        <View style={styles.headerInputBg}>
          <TouchableOpacity
            onPress={() => searchcountry(search_text)}
            style={styles.headericonBg}>
            <FontAwesome
              name="search"
              size={moderateScale(20)}
              color={colors.textColor}
            />
          </TouchableOpacity>
          <TextInput
            placeholder="Select country by name"
            value={search_text}
            placeholderTextColor={colors.textColor}
            style={styles.headerTextInput}
            onChangeText={text => searchcountry(text)}
          />
        </View>
      </View>
    );
  };

  const _renderEmpty = () => {
    return (
      <View
        style={{
          height: moderateScale(50),
          backgroundColor: colors.white,

          flex: 1,
          justifyContent: 'center',
        }}>
        <Text style={styles.notFoundText}>No Result Found</Text>
      </View>
    );
  };
  const _renderItem = ({section: section, row: row}) => {
    const country = country_data[section].items[row];
    return (
      <TouchableOpacity
        activeOpacity={0.95}
        onPress={() => {
          setDefaultCountry(country),
            setSearchText(''),
            setCountryData(countries),
            setVisible(false);
        }}
        style={styles.renderItemMainView}>
        <View style={styles.FlagNameView}>
          <Image
            source={{
              uri: `https://zoobiapps.com/country_flags/${country.code.toLowerCase()}.png`,
            }}
            style={styles.imgView}
          />
          <Text numberOfLines={1} ellipsizeMode="tail" style={styles.text}>
            {country.name}
          </Text>
        </View>
        <Text style={{...styles.text, marginRight: wp(5), textAlign: 'right'}}>
          (+{country.callingCode})
        </Text>
      </TouchableOpacity>
    );
  };

  const searchcountry = text => {
    setSearchText(text);
    const items = countries[0].items.filter(row => {
      const result = `${row.code}${row.name.toUpperCase()}`;
      const txt = text.toUpperCase();
      return result.indexOf(txt) > -1;
    });
    setCountryData([{header: 'countries', items: items}]);
  };

  return (
    <Modal
      style={styles.modalStyle}
      animationIn={'slideInUp'}
      animationOut={'slideOutDown'}
      animationInTiming={1000}
      backdropOpacity={0.3}
      animationOutTiming={700}
      hideModalContentWhileAnimating={true}
      backdropTransitionInTiming={500}
      backdropTransitionOutTiming={700}
      useNativeDriver={true}
      isVisible={visible}
      onBackdropPress={() => {
        setVisible(false);
      }}
      onBackButtonPress={() => {
        setVisible(false);
      }}>
      <LargeList
        showsHorizontalScrollIndicator={false}
        style={{flex: 1, padding: moderateScale(10)}}
        onMomentumScrollBegin={onScrollStart}
        onMomentumScrollEnd={onScrollEnd}
        contentStyle={{backgroundColor: '#fff'}}
        showsVerticalScrollIndicator={false}
        heightForIndexPath={() => moderateScale(49)}
        renderIndexPath={_renderItem}
        data={country_data}
        bounces={false}
        renderEmpty={_renderEmpty}
        renderHeader={_renderHeader}
        headerStickyEnabled={true}
        initialContentOffset={{x: 0, y: 600}}
      />
    </Modal>
  );
};
export default CountrySelectionModal;

const styles = StyleSheet.create({
  modalStyle: {
    margin: moderateScale(15),
    borderRadius: moderateScale(10),
    overflow: 'hidden',
    backgroundColor: '#fff',
    marginVertical: moderateScale(60),
    justifyContent: 'center',
  },
  headermainView: {
    height: moderateScale(105),
    backgroundColor: '#fff',
  },
  headerTextBg: {
    height: moderateScale(50),
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#fff',
  },
  headerTitle: {
    textAlign: 'center',
    fontFamily: fonts.Bold,
    fontSize: moderateScale(16),
    color: colors.textColor,
    textAlignVertical: 'center',
  },
  headerInputBg: {
    height: moderateScale(40),
    borderRadius: moderateScale(30),
    overflow: 'hidden',
    justifyContent: 'center',
    alignItems: 'center',
    paddingHorizontal: moderateScale(10),
    backgroundColor: colors.inputbgColor,
    flexDirection: 'row',
  },
  headericonBg: {
    backgroundColor: colors.inputbgColor,
    alignItems: 'center',
    justifyContent: 'center',
    width: moderateScale(40),
    height: moderateScale(40),
  },
  headerTextInput: {
    backgroundColor: colors.inputbgColor,
    height: moderateScale(30),
    flex: 1,
    paddingTop: 0,
    includeFontPadding: false,
    fontFamily: fonts.Medium,
    color: colors.textColor,
    paddingBottom: 0,
    paddingHorizontal: 0,
  },
  notFoundText: {
    fontFamily: fonts.Medium,
    textAlign: 'center',
    fontSize: moderateScale(14),
    textAlignVertical: 'center',
    color: colors.textColor,
  },
  renderItemMainView: {
    backgroundColor: colors.white,
    flexDirection: 'row',
    alignSelf: 'center',
    height: moderateScale(43),
    alignItems: 'center',
    justifyContent: 'space-between',
    width: wp(100) - moderateScale(30),
  },
  FlagNameView: {
    flexDirection: 'row',
    justifyContent: 'center',
    paddingLeft: moderateScale(12),
    alignItems: 'center',
  },
  imgView: {
    height: moderateScale(30),
    width: moderateScale(30),
    marginRight: moderateScale(10),
    borderRadius: moderateScale(30),
  },
  text: {
    fontSize: moderateScale(13),
    color: colors.textColor,
    marginLeft: 1,
    fontFamily: fonts.Medium,
  },
});
Engr.Aftab Ufaq
  • 3,356
  • 3
  • 21
  • 47
  • Thank you for the alternative. However, it still does not explain why Flat list prevents firing the event although already rendered or whether this is an issue with react native SVG library. – Jankapunkt Jan 16 '23 at 14:41
  • this is preformance issue. flatlist is not a good solution for very large list having item more than 20 . when all the items ate not rendered then touch event not working. i have easted many days on this. tried everything of flatlist optimization. but non work. – Engr.Aftab Ufaq Jan 17 '23 at 03:16
  • Hmmm, this seems to be contrary to what [the docs say](https://reactnative.dev/docs/virtualizedlist): "Virtualization massively improves memory consumption and performance of large lists by maintaining a finite render window of active items and replacing all items outside of the render window with appropriately sized blank space". – Jankapunkt Jan 17 '23 at 08:46