Reputation: 8413
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 start --no-dev --minify
then open in Expo GoReproduction:
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.
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.
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
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