Perfomance problem with Flatlist in React native - reactjs

What I want
I'm trying to use a FlatList to render an array of objects that comes from server(I'm using Apollo and Graphql to get data from server), but I'm getting a message from console:
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": 2392, "dt": 4527, "prevDt": 2777}
I followed the message and wrapped my renderItem in React.memo, I'm using functional components so I cant use PureComponent or shouldComponentUpdate, but after this the problem persist.
What I have
Component structure
HomeScreen (responsible for obtaining the data from the server)
└── ProductList (FlatList Wrapper)
└── ProductListItem (renderItem)
HomeScreen.js
// HomeScreen.js
function HomeScreen({navigation}) {
const {data} = useHomeScreenProductsQuery({}) // this hook get data from server with apollo useQuery method
// just print something on console but it can do something more complex
const handleOnPressItem = () => console.log('go to product')
return (
<ProductList
horizontal
numColumns={2}
products={data?.collection?.products?.edges.map(e => e.node) ?? []}
onPressItem={handleOnPressItem}
/>
)
}
ProductList.js
// ProductList.js
import React from 'react'
import {Dimensions, FlatList, View} from 'react-native'
import {ProductListItem} from '../ProductListItem'
function ProductList({
products,
horizontal = false,
numColumns = 1,
onPressItem = () => {},
}) {
const getLayoutManager = () => {
const layoutManager = {}
if (horizontal) {
layoutManager.horizontal = true
layoutManager.showsHorizontalScrollIndicator = false
layoutManager.ItemSeparatorComponent = ProductListItemSeparator
layoutManager.contentContainerStyle = {padding: spacing.base}
} else {
layoutManager.numColumns = numColumns
}
return layoutManager
}
const renderProductListItem = ({item}) => {
const columns = horizontal ? 2 : numColumns
return (
<ProductListItem
product={item}
itemContainerStyle={{
width: dimens.common.WINDOW_WIDTH / columns,
}}
columnWrapperStyle={{flexDirection: 'column'}}
onPressItem={onPressItem}
/>
)
}
return (
<FlatList
{...getLayoutManager()}
// Pass a changing value to the `key` prop to update the columns number on the fly
key={numColumns}
data={products}
renderItem={renderProductListItem}
/>
)
}
ProductListItem.js
// ProductListItem.js
function ProductListItem({
product,
itemContainerStyle = {},
columnWrapperStyle = {},
onPressItem = () => {},
}) {
const price = product.pricing?.priceRange?.start
const priceUndiscounted = product.pricing?.priceRangeUndiscounted?.start
const getProductPrice = () => {
if (equal(price, priceUndiscounted)) {
// render a price formated with $ symbol
return <TaxedMoney taxedMoney={price} defaultValue={0} category="h6" />
}
return (
<>
<View style={styles.priceContainer}>
<Text category="c2" appearance="hint">
Antes
</Text>
<TaxedMoney
taxedMoney={priceUndiscounted}
category="c2"
appearance="hint"
style={styles.priceUndiscounted}
/>
</View>
<TaxedMoney taxedMoney={price} category="h5" />
</>
)
}
const renderItemHeader = () => (
<View style={styles.itemHeader}>
// this is only a wrapper of react-native-fast-image library
<Thumbnail source={product} />
</View>
)
return (
<View style={itemContainerStyle}>
// component from #UI-kitten components library
<Card
onPress={onPressItem}
header={renderItemHeader}
style={[styles.row, columnWrapperStyle]}
>
{/*Card Body*/}
<Text
category="c1"
appearance="hint"
ellipsizeMode="tail"
numberOfLines={1}
>
{product.category?.name}
</Text>
<Text category="c1" ellipsizeMode="tail" numberOfLines={2}>
{product.name}
</Text>
{getProductPrice}
</Card>
</View>
)
}
export default React.memo(ProductListItem)

Related

how to make flatelist a reusable component

i want to make my flatlist reusable but i am facing some difficulties while passing props.
code for the reusable component
const ListItemView = function (props) {
console.log(props);
return (
<View>
<FlatList
//data={props.data}
keyExtractor={props.keyp}
renderItem={props.disptext}
/>
</View>
);
};
when i run console.log on the props i get this
{"disptext": undefined, "keyp": [Function anonymous]}
this is how i am passing props from the parent screen
const keyf = () => {
console.log('keyf');
//for the key extractor
return (item => item.index);
};
const rendertext = () => {
console.log('rendertext');
//for rerender function of the flatlist
({ item }) => {
return (
<View>
<Text>holaa</Text>
<Text>{item.name}</Text>
</View>
);
}
};
return (
<View style={style.container}>
<ListItemView
//data={con}
keyp={keyf()}
disptext={rendertext()}
/>
</View>
);
};
please help
You are directly calling the function but you just need to give a reference of it...so that it is called only on some event.
It should be like this:
<ListItemView
//data={con}
keyp={keyf}
disptext={rendertext}
/>
or
<ListItemView
//data={con}
keyp={()=>keyf()}
disptext={()=>rendertext()}
/>

How to set parent state from FlatList component?

I have a PaymentMethodsScreen screen. On this screen there is a FlatList with PaymentCardItem components inside. And there is a checkbox inside the PaymentCardItem. When this checkbox checked I would like to update selectedCardToken state of PaymentMethodsScreen. But unfortunately I couldn't figure out how to do it. I tried to pass props but I was doing it wrong. Here is my code (without passing props).
How can I achieve that? Thank you very much for your helps.
const PaymentCardItem = ({ family, association, bin_number, token, isSelected }) => (
<View>
<RadioCheckbox
selected={ isSelected }
onPress={ () => this.setSelectedCardToken(token) // Something wrong here }
/>
<Text>{family}, {association}</Text>
<Text>{bin_number}**********</Text>
</View>
);
const PaymentMethodsScreen = ({navigation}) => {
const {state} = useContext(AuthContext);
const [cardList, setCardList] = useState(null) // This stores card list data from API request
const [selectedCardToken, setSelectedCardToken] = useState('test token')
const renderItem = ({ item }) => (
<PaymentCardItem
bin_number={item.bin_number}
family={item.family}
association={item.association}
token={ item.token }
isSelected={ (selectedCardToken == item.token) }
/>
);
return (
<SafeAreaView>
<View>
<FlatList
data={cardList}
renderItem={renderItem}
keyExtractor={item => item.alias}
/>
</View>
</SafeAreaView>
);
};
add onPress prop to PaymentCardItem:
// PaymentMethodsScreen
<PaymentCardItem
onPress={() => setSelectedCardToken(item.token)}
>
I don't know how the PaymentCardItem component is structured, but generally you should add onPress prop on the TouchableOpacity in the component or whatever is your onPress handler:
// PaymentCardItem component
<TouchableOpacity
onPress={() => props.onPress()}
>
You can pass down the handler function which gets called on checkbox being checked or unchecked to your PaymentCardItem component.
You can also pass setSelectedCardToken directly, but in case you have some extra logic before you update state, it's better to have a handler for more readability.
So, the code will be like below.
const PaymentMethodsScreen = ({ navigation }) => {
const { state } = useContext(AuthContext);
const [cardList, setCardList] = useState(null) // This stores card list data from API request
const [selectedCardToken, setSelectedCardToken] = useState('test token')
const handleCardTokenSelection = (isTokenSelected) => {
if(isTokenSelected) {
setSelectedCardToken(); // whatever logic you have
} else {
setSelectedCardToken(); // whatever logic you have
}
}
const renderItem = ({ item }) => (
<PaymentCardItem
bin_number={item.bin_number}
family={item.family}
association={item.association}
token={ item.token }
isSelected={ (selectedCardToken == item.token) }
handleCardTokenSelection={handleCardTokenSelection}
/>
);
return (
<SafeAreaView>
<View>
<FlatList
data={cardList}
renderItem={renderItem}
keyExtractor={item => item.alias}
/>
</View>
</SafeAreaView>
);
};
const PaymentCardItem = ({ family, association, bin_number, token, isSelected, handleCardTokenSelection }) => (
<View>
<RadioCheckbox
selected={ isSelected }
onPress={handleCardTokenSelection}
/>
<Text>{family}, {association}</Text>
<Text>{bin_number}**********</Text>
</View>
);
You need to set the state for PaymentCardItem not for the whole Flatlist, to show the item is selected.
I think you update the PaymentCardItem component to something like the below code(You can update the logic as per requirement)
class PaymentCardItem extends React.Component {
constructor(props) {
super(props);
this.state = {selectedCardToken: "", isSelected: false};
}
setSelectedCardToken=(token)=>{
if(selectedCardToken == token){
this.setState({
selectedCardToken: token,
isSelected: true
})
}
}
render() {
const { family, association, bin_number, token }=this.props;
const { isSelected } = this.state;
return (
<View>
<RadioCheckbox
selected={ isSelected }
onPress={ () => this.setSelectedCardToken(token)
/>
<Text>{family}, {association}</Text>
<Text>{bin_number}**********</Text>
</View>
);
}
}

is there a way to memoize a function that returns a random color in every iteration in react

I am building a react native contact app that generates a random color for every contact number. I want to memoize the returned value of the random color generator function on every iteration so when I am searching the contacts, the color won't be changing on every action.
currently, I am using react's useMemo to memoize the function result, but I am getting an invalid hook because I can not use hooks inside a normal function with a loop.
As my code below the useMemo hook is called only on the first render or when the component re-renders. I am only able to return one value because I can't use useMemo inside the map.
here is my code ⬇. Thanks for helping
const Contacts = () => {
const [contact, setContact] = useState({});
const randomColor = useMemo(() => randomColorGenerator(), []);
useEffect(() => {}, []);
const renderContacts = item => {
return item.phoneNumbers.map(element => (
<TouchableOpacity
activeOpacity={1}
key={element.digits.toString()}
}}
>
<View>
<View>
<Text>{item.firstName}</Text>
<Text>{element.digits}</Text>
</View>
</View>
</TouchableOpacity>
));
};
const renderList = () => {
return (
<FlatList
keyboardShouldPersistTaps="handled"
data={contact}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => {
return <View>{renderContacts(item)}</View>;
}}
/>
);
};
return (
<View>
<Text style={Styles.textStyle}>All Contacts</Text>
{renderList()}
</View>
);
};
Make the function - a React functional component. Then you'll be able to use React.useMemo inside it. Also, this answer and this answer might help clarify better
// define it as a functional component
const RenderContacts = ({ item }) => {
// useMemo inside that
const randomColor = useMemo(() => randomColorGenerator(), []);
// TODO: use randomColor somewhere
return item.phoneNumbers.map(element => (
<TouchableOpacity ...>
...
</TouchableOpacity>
));
)
}
const Contacts = () => {
...
// extract this out into its own component
// const renderContacts = item => {
// return item.phoneNumbers.map(element => (
// <TouchableOpacity
// activeOpacity={1}
// key={element.digits.toString()}
// }}
// >
// <View>
// <View>
// <Text>{item.firstName}</Text>
// <Text>{element.digits}</Text>
// </View>
// </View>
// </TouchableOpacity>
// ));
// };
const renderList = () => {
return (
<FlatList
...
renderItem={({ item }) => {
// return <View>{renderContacts(item)}</View>;
// render the component, don't call it as a function
return <View><RenderContacts item={item} /></View>;
}}
/>
);
};
return (
<View>
<Text style={Styles.textStyle}>All Contacts</Text>
{renderList()}
</View>
);
};
I would just suggest creating your own pure-javascript wrapper around randomColorGenerator that caches values based on a key to give a consistent result. Something like:
const memoizedRandomColorGenerator = function() {
const cache = {};
return function(k) {
if (typeof cache[k] !== 'undefined') {
return cache[k];
}
cache[k] = randomColorGenerator();
return cache[k];
}
}() // note that we are immediately invoking this function to close over the cache
Define that somewhere outside of your component so it is only created once.
Then use it in component
const renderContacts = item => {
return item.phoneNumbers.map(element => (
<TouchableOpacity
activeOpacity={1}
key={element.digits.toString()}
}}
>
<View style={{
backgroundColor: memoizedRandomColorGenerator(element.digits.toString())
}}>
<View>
<Text>{item.firstName}</Text>
<Text>{element.digits}</Text>
</View>
</View>
</TouchableOpacity>
));
};
Color can be either property of the contact, that can be set earlier and stored inside the contact.
Or color can be function of the ${contact.firstName} ${contact.lastName}.
You can get hexadecimal hash from the string, then color from hash.
This way you get persistent assignment between contacts and colors.
useMemo is intended for heavy calculations, that returns the same result, not random.

React Native Component will receive is deprecated

I'm creating a new react native app after writing some code I got this warning :
in the emulator.
But I don't see where the problem is.
This is my code - App.js:
const IS_ANDROID = Platform.OS === 'android';
const SLIDER_1_FIRST_ITEM = 1;
class App extends Component {
constructor (props) {
super(props);
this.state = {
slider1ActiveSlide: SLIDER_1_FIRST_ITEM
};
}
_renderItem ({item, index}) {
return <SliderEntry data={item} even={(index + 1) % 2 === 0} />;
}
_renderItemWithParallax ({item, index}, parallaxProps) {
return (
<SliderEntry
data={item}
even={(index + 1) % 2 === 0}
parallax={true}
parallaxProps={parallaxProps}
/>
);
}
_renderLightItem ({item, index}) {
return <SliderEntry data={item} even={false} />;
}
_renderDarkItem ({item, index}) {
return <SliderEntry data={item} even={true} />;
}
mainExample (number, title) {
const { slider1ActiveSlide } = this.state;
return (
<View style={styles.exampleContainer}>
<Carousel
ref={c => this._slider1Ref = c}
data={ENTRIES1}
renderItem={this._renderItemWithParallax}
sliderWidth={sliderWidth}
itemWidth={itemWidth}
hasParallaxImages={true}
firstItem={SLIDER_1_FIRST_ITEM}
inactiveSlideScale={0.94}
inactiveSlideOpacity={0.7}
// inactiveSlideShift={20}
containerCustomStyle={styles.slider}
contentContainerCustomStyle={styles.sliderContentContainer}
loop={true}
loopClonesPerSide={2}
autoplay={true}
autoplayDelay={4000}
autoplayInterval={3000}
onSnapToItem={(index) => this.setState({ slider1ActiveSlide: index }) }
/>
</View>
);
}
get gradient () {
return (
<LinearGradient
colors={[colors.background1, colors.background2]}
startPoint={{ x: 1, y: 0 }}
endPoint={{ x: 0, y: 1 }}
style={styles.gradient}
/>
);
}
render () {
const example1 = this.mainExample(1);
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.container}>
<StatusBar
translucent={true}
backgroundColor={'rgba(0, 0, 0, 0.3)'}
barStyle={'light-content'}
/>
{ this.gradient }
<ScrollView
style={styles.scrollview}
scrollEventThrottle={200}
directionalLockEnabled={true}
>
{ example1 }
</ScrollView>
</View>
</SafeAreaView>
);
}
}
export default App;
All I used is this carousel library https://github.com/archriss/react-native-snap-carousel nothing else but I don't know what I am doing wrong in this case
and is it really the code isn't going to work in the future ?
As said by the warning, componentWillReceiveProps is deprecated.
The component react-native-snap-carousel use that feature and is deprecated in the latest version of react-native.
You have to either change the node_modules/react-native-snap-carousel to use componentDidUpdate, use another component for carousel or disable the warning until the carousel maintainers updates their package.
To disable it you can do, inside you App.js, in the constructor:
import {YellowBox} from 'react-native'; //import it
YellowBox.ignoreWarnings(['Warning: componentWillReceiveProps']);
Hope this helps you!
EDIT.
About the other Warning you've got. That happens when you do a setState when a component has already been unmounted. Make sure that recreates that scenario. I would suggest to not ignore this warning using YellowBox but solve it.

Button not displaying fetch results to the component?

I am creating a currency converter app and it will retrieve currency value from the API and multiply with the text input for the result. Both the API result and Text input are stored in State and passing as props to the Component
import React from 'react';
import { StyleSheet, Text, View,TextInput,Button } from 'react-native';
import DisplayResult from './src/DisplayResult'
export default class App extends React.Component {
state = {
currency:'',
pokeList: '',
}
placeNameChangeHandler=(val)=>{
this.setState({currency:val});
}
// console.log(this.state.currency);
async findCurrency () {
try {
//Assign the promise unresolved first then get the data using the json method.
const pokemonApiCall = await fetch('https://free.currconv.com/api/v7/convert?q=KWD_INR&compact=ultra&apiKey={my_api_Key}');
const pokemon = await pokemonApiCall.json();
this.setState({pokeList: pokemon['KWD_INR']});
// console.log(pokemon);
} catch(err) {
console.log("Error fetching data-----------", err);
};
<DisplayResult convert={this.state.pokeList} result={this.state.currency} />
}
render() {
return (
<View style={styles.container}>
<TextInput
placeholder="Currency"
value = {this.state.currency}
onChangeText={this.placeNameChangeHandler}
/>
<Button
title="Search"
onPress={this.findCurrency.bind(this)}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
DisplayResult
const DisplayResult =(props)=>{
const {convert,result} = props
console.log(convert);
return (
<View>
<Text>{result*convert}</Text>
</View>
)
}
export default DisplayResult;
I am trying to pass the API result and text input to the display component and this will multiply the values and will give the result.
Now this is not functioning or giving result
why this is not showing and where it's going wrong?
In your findCurrency method you just "call" the DisplayResult without returning it, but I don't think this is the good method to display your result.
You can use your component directly within the render method by testing your state variables, like this :
findCurrency = async () => {
try {
const pokemonApiCall = await fetch(
"https://free.currconv.com/api/v7/convert?q=KWD_INR&compact=ultra&apiKey={my_api_Key}"
);
const pokemon = await pokemonApiCall.json();
this.setState({ pokeList: pokemon["KWD_INR"] }); // You set your "pokeList" variable up
} catch (err) {
console.log("Error fetching data-----------", err);
}
}
Note that you remove the DisplayResult call here and the function became an arrowed function, then in your render method use the test to make your result appear only if pokeList isn't empty :
render() {
return (
<View style={styles.container}>
<TextInput
placeholder="Currency"
value={this.state.currency}
onChangeText={this.placeNameChangeHandler}
/>
<Button title="Search" onPress={this.findCurrency.bind(this)} />
{this.state.pokeList !== "" && (
<DisplayResult
convert={this.state.pokeList}
result={this.state.currency}
/>
)}
</View>
);
}
Then, you don't have to bind your function in the onPress method like this, JavaScript immediately calls the function if you do this, instead, use arrow functions, you can access this by doing so in your function AND the onPress method doesn't call it if you don't click on the button, you just have to specify which function to execute when clicked :
<Button title="Search" onPress={this.findCurrency} />
If you have parameters in your function, use an arrow function instead :
<Button title="Search" onPress={() => yourFunction(param)} />
This should do the trick.
Try writing your function like that :
const findCurrency = async() => {
// ...
};
and call it like that
<Button
title="Search"
onPress={() => this.findCurrency()}
/>
I personnaly never use .bind because I think this is very unclear.
try using conditional rendering,
if data fetched, then only render.
import React from 'react';
import { StyleSheet, Text, View,TextInput,Button } from 'react-native';
import DisplayResult from './src/DisplayResult'
export default class App extends React.Component {
state = {
currency: '',
pokeList: '',
}
placeNameChangeHandler=(val)=>{
this.setState({currency:val});
}
// console.log(this.state.currency);
this.findCurrency.bind(this);
async findCurrency () {
try {
//Assign the promise unresolved first then get the data using the json method.
const pokemonApiCall = await fetch('https://free.currconv.com/api/v7/convert?q=KWD_INR&compact=ultra&apiKey={my_api_Key}');
const pokemon = await pokemonApiCall.json();
this.setState({pokeList: pokemon['KWD_INR']});
// console.log(pokemon);
} catch(err) {
console.log("Error fetching data-----------", err);
};
}
render() {
return (
<View style={styles.container}>
<TextInput
placeholder="Currency"
value = {this.state.currency}
onChangeText={this.placeNameChangeHandler}
/>
<Button
title="Search"
onPress={this.findCurrency()}
/>
</View>
{
if(this.state.pokeList !== '' || this.state.currency !== '') ?
<DisplayResult convert={this.state.pokeList} result={this.state.currency} /> : <div></div>
}
);
}
}

Resources