Jankapunkt
Jankapunkt

Reputation: 8413

React Native Flatlist element onPress not fired until List rendering is complete

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:

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.

Upvotes: 5

Views: 769

Answers (1)

Engr.Aftab Ufaq
Engr.Aftab Ufaq

Reputation: 6212

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,
  },
});

Upvotes: 0

Related Questions