Prevent FlatList re-rendering causing performance problems - react-native-flatlist

I'm building a React Native App using Hooks and ES6.
The Home screen fetches some data from API and display it as image galleries.
The Home screen is the parent component which includes a slideshow gallery and a Flatlist as children.
1) Slideshow - image gallery autoplay that run some operations (fetching URL in case user clicks on them) on each image iteration (every 3 secs) - This causes the re-render on the entire parent components and its children but it renders every 3 secs as supposed
2) Flatlist - just a simple Flatlist that takes an array of images - this should only rendered once when the parent component is initially loaded first
The problem
Every 3 seconds a new image is displayed in the slideshow, a function runs to fetch some props for the image displayed, the parent component is re-rendered but there is no need for Flatlist in the second gallery to run again since the array of images is the same as initially loaded (not changed)
This is my Parent code
const getTopTenMovies = (state) => state.moviescreen.popularmovies //fetching data from Redux
const upcomingMovies = (state) => state.moviescreen.upcomingmovies
const MoviesScreen = (props) => {
const topTenMovies = useSelector(getTopTenMovies);
const [imageIndex, setImageIndex] = useState("");
/* useEffect below runs every time imageIndex changes (a new image is displayed every 3 secs) */
useEffect(() => {
setCarouselMovieId(selectedImageItem.id);
setCarouselMovieTitle(selectedImageItem.title);
}, [imageIndex]);
/* user clicks on an image and is taken to another screen
const fetchMovieHandler = async (movieId, movieTitle) => {
props.navigation.navigate({
routeName: "MovieDetail",
params: {
assetId: movieId,
assetName: movieTitle,
},
});
};
return (
<ImageGallery
data={topTenMovies}
currentImage=setImageIndex(index)
...
/>
<View style={styles.containerRow}>
<FlatList
horizontal={true}
data={upcomingMovies}
initialNumToRender={5}
windowSize={10}
removeClippedSubviews={true}
keyExtractor={(item) => item.id.toString()}
renderItem={(itemData) => (
<HomeItem
id={itemData.item.id}
poster={itemData.item.poster_path}
onAssetSelection={fetchMovieHandler}
/>
)}
/>
</View>
</View>
)
}
The component renders the individual images within the Flatlist and passes the parameters (movieId, movieTitle) to the fetchMovieHandler function.
HomeItem code below...
class HomeItem extends React.PureComponent {
render() {
const assetHandler = async () => {
this.props.onAssetSelection(this.props.id, this.props.title);
};
console.log("HomeItem is called");
return (
<TouchableOpacity onPress={assetHandler}>
<View>
<Image
key={this.props.id}
source={{
uri: `https://*******${this.props.poster}`,
}}
/>
</View>
</TouchableOpacity>
);
}
}
export default React.memo(HomeItem);
Every 3 seconds the HomeItem is called and I see 10 log entries (one for each image and render - upcomingMovies has 10 images). From the UI everything looks fines since the images dont seems to change probably because I've defined the HomeItem as PureComponent and using React.memo(HomeItem) but it is causing performance issues since I'm getting the following error:
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 tried to include the Flatlist inside the HomeItem component but the problem persists.

Following further research and thanks to #landorid suggestion I figured out that I need control the renderItem of the FlatList by declaring it in a separate function with a useCallback hook and dependency on my data source.
Tried to include Flatlist in a separate PureComponent my the re-rendering kept happening.
The ideal solution is to change this code
renderItem={(itemData) => (
<HomeItem
id={itemData.item.id}
poster={itemData.item.poster_path}
onAssetSelection={fetchMovieHandler}
/>
)}
to something like this
.....
const renderRow = ({ itemData }) => {
return (
<HomeItem
id={itemData.item.id}
poster={itemData.item.poster_path}
title={itemData.item.title}
onAssetSelection={fetchMovieHandler}
/>
);
};
keyExtractor = (item) => item.id.toString();
return (
<FlatList
horizontal={true}
data={upcomingMovies}
initialNumToRender={5}
windowSize={10}
removeClippedSubviews={true}
keyExtractor={keyExtractor}
renderItem={renderRow}
/>
and then wrapping the renderRow() function in a useCallback hook.
The problem is that my data source data={upcomingMovies} is an array of objects with 2 levels and I need to use itemData.item.title to retrieve the title value for example.
While it works in my current renderItem settings it won't work when I use it in an external function as in
const renderRow = ({ itemData }) => {
return (
<HomeItem
id={itemData.item.id}
poster={itemData.item.poster_path}
title={itemData.item.title}
onAssetSelection={fetchMovieHandler}
/>
);
};
I simply get, "item" variable not available
Any suggestion on how to modify the function?

If I were you I would change the following:
Don't place inline functions in FlatList (keyExtractor, renderItem)! Place it to separated function.
Use simple Component extension instead of PureComponent and compare your props with `shouldComponentUpdate. (I think the passed function called onAssetSelection causes the re-render. Check it.)
You don't need to use React.memo because it is for functional components.

Related

Konva Image showing black when loading

I'm trying to have a grid as a background using ImageKonva and filling it with a pattern.
The problem is that when I load the canvas, the ImageKonva looks completely black, and it shows up as soon as I add children elements.
import React from 'react'
import { Stage, Layer, Image as ImageKonva } from 'react-konva'
const Canvas = ({ children, width, height, onClick }) => {
const image = new Image()
// this hash is the image of a grid
image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAIElEQVQoU2NkIAK8e/fuPyMR6hhGFeINJXDwgAhiwhIAOZAl5YYZd5AAAAAASUVORK5CYII='
return (
<Stage width={width} height={height}>
<Layer>
<ImageKonva
preventDefault={false}
width={width / 2}
height={width / 2}
fillPatternImage={image}
onClick={onClick}
/>
{children}
</Layer>
</Stage>
)
}
I've created this codesandbox where you can see the issue: https://codesandbox.io/s/angry-river-kszh0?file=/src/App.js
Note: It's not always reproducible, (can't figure out what's the pattern).
Sometimes it wouldn't happen, and I need to open chrome's console and refresh the page to make it happen.
You'll see the black box, and it will only show the grid after you click the button. The button only adds a child element.. from there, the image would always show. This problem only happens during the first load.
I believe I've figured it out, it seems I didn't consider that the image could be added asynchronously... so when I load the canvas, the image is not there yet.
So the solution is to only load ImageKanvas once the image is loaded using by setting an onload method to the image object
const Canvas = ({ children, width, height, onClick }) => {
const [loadGrid, setLoadGrid] = useState(false);
const image = new Image();
// this hash is the image of a grid
image.src =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAIElEQVQoU2NkIAK8e/fuPyMR6hhGFeINJXDwgAhiwhIAOZAl5YYZd5AAAAAASUVORK5CYII=";
image.onload = () => {
setLoadGrid(true);
};
return (
<Stage width={width} height={height}>
<Layer>
{loadGrid && (
<ImageKonva
preventDefault={false}
width={width / 2}
height={width / 2}
fillPatternImage={image}
onClick={onClick}
/>
)}
{children}
</Layer>
</Stage>
);
};
https://codesandbox.io/s/wild-wind-br2qo?file=/src/App.js

Dynamically updating the title component for a Material UI Tooltip

I am using a React Material UI Tooltip component. Specifically :
render() {
const {comment} = this.props;
...
<Tooltip title={this.handleFlaggedComment(comment)}>
<IconButton className={classes.tooltipIconButton} aria-label="Reported" href="">
<ReportedIcon className={classes.reportedIcon} />
</IconButton>
</Tooltip>
...
}
The handleFlaggedComment is implemented as :
handleFlaggedComment = (comment) => {
return (
<Fragment>
<div>
REPORTED COUNT : {comment.reportedCount}
</div>
</Fragment>
)
}
Lets also assume there is a mapStateToProps which is implemented as :
mapStateToProps(state) {
return {
comment: state.commentsList.currentComment
}
}
When the reportedCount goes from 1 to say 3, and I hover over the icon to show the tooltip, I would expect it to show 3 as the reportedCount. However it only shows 1 , which was the initial value
Is there a way to make the tooltip title update and show the updated comment, specifically the correct reportedCount ?
let us also assume that some UI action is triggering the reportedCount to increase. An example, is clicking on an icon that increments the reportedCount by 1 and it is updating state in redux for 'currentComment'

React Native WebView pre-render for faster performance - how to do it?

In React Native, when using the WebView component, it starts to load the external content at the moment when the component will be rendered.
To increase performance in the application, I have tried to pre-fetch the external HTML so that it is ready when the component will be rendered. It seems like it is only an actual call to the render method will cause the loading to start and this is only controlled by what is rendered on the screen. I suppose React Native has no concept of shadow DOM that could be used to call the render method a head of time.
Trying to manipulate the lifecycle methods does also not work and is probably not a correct way of doing it either?
I have also tried to do a fetch() of the external HTML-content, with the right user-agent in the header, and pass the responseText to the WebComponent. This sometimes works for some sites of sources, but for others i run into ACAP (Automated Content Access Protocol) issues, to this is not the preferred solution.
Is there a way to pre-fetch external HTML content to a WebView component so that it displays faster?
fetch method runs on react side, fetch keep cache but that available for react apis and there component. WebView has there own caching concept. It's a browser. Caching of fetch will not avaialble for WebView. For faster loading by pre loaded data, You should fetch data by WebView instance of fetch api.
You can create a hidden WebView by setting width and height 0 and load your site on that. This will load your site on ViewView and keep cache, that will available for next time loading.
Here is a sample
import React, { Component } from 'react';
import {
Platform,
StyleSheet,
Text,
View,
WebView,
Alert,
ActivityIndicator,
} from 'react-native';
// const url = 'https://github.com/facebook/react-native'
// const url = 'https://in.yahoo.com/?p=us'
const url = 'https://www.facebook.com/'
class TestWebView extends Component {
render() {
var renderTime = Date.now();
return (
<WebView
source={{uri: url}}
style={{marginTop: 20, flex: 1}}
onLoad={() => {
Alert.alert('On load event', `Loading time : ${Date.now() - renderTime}`)
}}
/>
);
}
}
export default class App extends Component<{}> {
constructor(props) {
super(props)
this.state = {
isLoaded: false,
}
}
render() {
if (this.state.isLoaded) {
return (
<TestWebView />
)
}
return (
<View style={styles.container}>
<View style={{height: 0, width: 0}}>
<WebView
source={{uri: url}}
onLoad={() => {
this.setState({isLoaded: true})
}}
/>
</View>
<ActivityIndicator />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
});
I test this. After first time loaded data on WebView, It reduce 70% loading on actual WebView where we want to show to user.

Get state in React Native IOS Tab Bar

I'm a bit new to react-native, so I might not have understood this while reading the docs but. I have a tab bar in an ios app with 2 tabs.
Currently the view code for one of the tabs is in the same file as the page that loads the the Tab bar (I call this HomeTab). The second tab (call Tab2) which contains the view code for the second tab is in a separate .js page and is loaded into HomeTab.
I'm having trouble passing information in HomeTab's state to Tab2, and do not want to write all of Tab2 into HomeTab like I did for Tab1 because I will get a really big file.
The code structure is a bit like this:
{View Code for Tab 1 here}
<TabBarIOS selectedTab={this.state.selectedTab}>
<TabBarIOS.Item
selected={this.state.selectedTab === 'Tab1'}
title = "Tab1"
icon = {require('image!Tab1')}
onPress={() => {
this.setState({
selectedTab: 'Tab1'
}
);
}}>
// Call Tab1 View here
</TabBarIOS.Item>
<TabBarIOS.Item
selected={this.state.selectedTab === 'Tab2'}
title = "Tab2"
icon = {require('image!Tab2')}
onPress={() => {
this.setState({
selectedTab: 'Tab2'
}
);
}}>
<Tab2/>
</TabBarIOS.Item>
Intuitively I feel like all I am doing is putting code in a separate file, so both tabs should have the same state, but when I print out anything from my state it is undefined. Any advice would be appreciated! Thanks.
I was fairly ignorant of the capabilities of javascript. I was able to pass props just by including it in the call to Tab2 like this
<Tab2 propName = {some information}/>

React native Image variable in name doesn't work

I'm trying to load an image that has a variable in it's source, something like this.
<View>
{_.map(this.state.ambiences, (id) => {
var image = require('../../assets/img/ambiances/icons/' + label + '.png'); // variable label equals "chic" here
var image2 = require('../../assets/img/ambiances/icons/chic.png');
return <Image style={styles.pastilleOverlay} source={image} />;
})}
</View>
I get the following error (thrown when trying to set variable image) : Requiring unknown module "../../assets/img/ambiances/icons/chic.png"
If I comment the var image = ... line, it works fine with source={image2} in the Image tag.
I checked, both strings in the are exactly the same. Any ideas ?
Workaround
Maybe this Issue could help you.
We intentionally don't support dynamically generated image names because it makes it very hard to manage assets over time, just like how you wouldn't want to do
var MyComponent = require('./' + libName + typeOfComponent);
or something like that. Dynamic image name generation like this also
won't work with the new dynamic asset managing system we're rolling
out which let's you add and update assets on the fly with just cmd+R
(no more need to add to Xcode and recompile).
Bundled Images
The images you want to use, need to be bundled "via Xcode asset catalogs or Android drawable folder", as the documentation says. Also you have to specify the dimensions of the images manually.
Note that this approach provides no safety checks. It's up to you to guarantee that those images are available in the application.
<View>
{_.map(this.state.ambiences, (id) => {
return <Image style={styles.pastilleOverlay} source={{ uri: '../../assets/img/ambiances/icons/' + label + '.png' }} style={{width: 40, height: 40}} />;
})}
</View>
Alternative
What do you think about using a switch- or if-statement?
<View>
{_.map(this.state.ambiences, (id) => {
switch (label) {
case "chic":
return <Image style={styles.pastilleOverlay} source={require('../../assets/img/ambiances/icons/chic.png')} />;
case "otherimage":
return <Image style={styles.pastilleOverlay} source={require('../../assets/img/ambiances/icons/otherimage.png')} />;
default:
return <Image style={styles.pastilleOverlay} source={require('../../assets/img/ambiances/icons/default.png')} />;
}
})}
</View>
This might be little late, but if you have to have to change image source, update the key for image element. It will treat the re-rendered images as a new component and will require the image correctly.
Simply using image name in the key, did the trick.

Resources