react native Why does my async code hang the rendering? - reactjs

General :
TL;DR: async code hangs rendering.
I have this component with a Modal and inside the Modal it renders a list of filters the user can choose from. When pressing a filter the color of the item changes and it adds a simple code(Number) to an array. The problem is that the rendering of the color change hangs until the logic that adds the code to the array finishes.
I don't understand why adding a number to an array takes between a sec and two.
I don't understand why the rendering hangs until the entire logic behind is done.
Notes: I come from a Vue background and this is the first project where I'm using react/react-native. So if I'm doing something wrong it would be much appreciated if someone points that out
Snack that replicates the issue :
Snack Link
My code for reference :
I use react-native with expo managed and I use some native-base components for the UI.
I can't share the whole code source but here are the pieces of logic that contribute to the problem :
Parent : FilterModal.js
The rendering part :
...
<Modal
// style={styles.container}
visible={modalVisible}
animationType="slide"
transparent={false}
onRequestClose={() => {
this.setModalVisible(!modalVisible);
}}
>
<Center>
<Pressable
onPress={() => this.setModalVisible(!modalVisible)}
>
<Icon size="8" as={MaterialCommunityIcons} name="window-close" color="danger.500" />
</Pressable>
</Center>
// I use sectionList because the list of filters is big and takes time to render on the screen
<SectionList
style={styles.container}
sections={[
{ title: "job types", data: job_types },
{ title: "job experience", data: job_experience },
{ title: "education", data: job_formation },
{ title: "sector", data: job_secteur }
]}
keyExtractor={(item) => item.id}
renderItem={({ item, section }) => <BaseBadge
key={item.id}
pressed={this.isPressed(section.title, item.id)}
item={item.name}
code={item.id}
type={section.title}
add={this.addToFilters.bind(this)}
></BaseBadge>}
renderSectionHeader={({ section: { title } }) => (
<Heading color="darkBlue.400">{title}</Heading>
)}
/>
</Modal>
...
The logic part :
...
async addToFilters(type, code) {
switch (type) {
case "job types":
this.addToTypesSelection(code);
break;
case "job experience":
this.addToExperienceSelection(code);
break;
case "formation":
this.addToFormationSelection(code);
break;
case "sector":
this.addToSectorSelection(code);
break;
default:
//TODO
break;
}
}
...
// the add to selection methods look something like this :
async addToTypesSelection(code) {
if (this.state.jobTypesSelection.includes(code)) {
this.setState({ jobTypesSelection: this.state.jobTypesSelection.filter((item) => item != code) })
}
else {
this.setState({ jobTypesSelection: [...this.state.jobTypesSelection, code] })
}
}
...
Child :
The rendering Part
render() {
const { pressed } = this.state;
return (
< Pressable
// This is the source of the problem and read further to know why I used the setTimeout
onPress={async () => {
this.setState({ pressed: !this.state.pressed });
setTimeout(() => {
this.props.add(this.props.type, this.props.code);
});
}}
>
<Badge
bg={pressed ? "primary.300" : "coolGray.200"}
rounded="md"
>
<Text fontSize="md">
{this.props.item}
</Text>
</Badge>
</Pressable >
);
};
Expected outcome :
The setState({pressed:!this.state.pressed}) finishes the rendering of the item happens instantly, the rest of the code happens after and doesn't hang the rendering.
The change in the parent state using the add code to array can happen in the background but I need the filter item ui to change instantly.
Things I tried :
Async methods
I tried making the methods async and not await them so they can happen asynchronously. that didn't change anything and seems like react native ignores that the methods are async. It hangs until everything is done all the way to the method changing the parent state.
Implementing "event emit-listen logic"
This is the first app where I chose to use react/react-native, coming from Vue I got the idea of emitting an event from the child and listening to it on the parent and execute the logic that adds the code to the array.
This didn't change anything, I used eventemitter3 and react-native-event-listeners
Using Timeout
This is the last desperate thing I tried which made the app useable for now until I figure out what am I doing wrong.
basically I add a Timeout after I change the state of the filter component like so :
...
< Pressable
onPress={async () => {
// change the state this changes the color of the item ↓
this.setState({ pressed: !this.state.pressed });
// this is the desperate code to make the logic not hang the rendering ↓
setTimeout(() => {
this.props.add(this.props.type, this.props.code);
});
}}
>
...
Thanks for reading, helpful answers and links to the docs and other articles that can help me understand better are much appreciated.
Again I'm new to react/react-native so please if there is some concept I'm not understanding right point me in the right direction.

For anyone reading this I finally figured out what was the problem and was able to solve the issue for me.
The reason the rendering was getting hang is because the code that pushes to my array took time regardless of me making it async or not it was being executed on the main thread AND that change was triggering screen re-render which needed to wait for the js logic to finish.
The things that contribute to the solution and improvement are :
Make the array (now a map{}) that holds the selected filters stateless, in other words don't use useState to declare the array, instead use good old js which will not trigger any screen re-render. When the user applies the filters then push that plain js object to a state or context like I'm doing and consume it, doing it this way makes sure that the user can spam selecting and deselecting the filters without hanging the interactions.
first thing which is just a better way of doing what I needed is to make the array a map, this doesn't solve the rerender issue.

Related

Slide API Mui in React is triggering listener at start not end

I want to have an object slide to the left side of the screen from the right. This is my code:
import rock1 from "src/demos/MyDemo/assets/rock1.png"
import Slide from '#mui/material/Slide';
const Test = () => {
return (
<>
<Slide
direction="left"
in={true}
mountOnEnter unmountOnExit
timeout={4000}
easing={{
enter: "linear",
exit: "linear"
}}
addEndListener={()=>alert("Done")}
>
<img
height={100}
className="Rock" src={rock1}
style={{position:"absolute", left: -100, marginBottom:20}}
/>
</Slide>
</>
);
}
export default Test;
I want to do some stuff when the transition ends, so I've added a placeholder end listener for now. However, the listener triggers when the transition starts and does not trigger again when it ends. Why is it happening at the beginning, not the end, and how can I get it to register correctly?
According to the react-transition-group doc that is referenced by MUI on the Slide API page, addEndListener can be used to add another listener for the transition end and pass a callback.
Live demo for below examples on stackblitz.
Perhaps try below example for addEndListener:
addEndListener={(node, done) =>
// 👇 node is the element being transitioned
node.addEventListener(
// 👇 This event fires at the finish of transition
"transitionend",
(e) => {
// 👇 Do something here
console.log("Actually done");
done(e);
},
false
)
}
However, according to the above sources, it seems that another way to achieve the same result here is by adding a callback to the prop onEntered:
onEntered={() => console.log('onEntered fired')}
Despite the name, onEntered is fired when the transition is complete here, while another prop onExited runs when the exit transition concludes, if applicable. More details can be found in above source links.
Hope this will help.

Recording user interactions in React Native / Snap Carousel - firstItem

I have been having this issue for a couple of days and cannot seem to find a solution.
I would like to record the user's interaction on a database. I am displaying data using a React Native Snap Carousel, and need to record if the item was viewed by the user. To do this, and as described in other stack overflow questions, I am using "onSnapToItem". onSnapToItem is triggered everytime I change from one slide to another. Then, if the user views the slide for more than 2 seconds, I count that as an interaction.
The problem is that onSnapToItem is not triggered on the firstItem (which makes sense, because I am not changing slide). Can anybody think of a solution?
<Carousel
vertical
layout={"default"}
ref={_carousel}
data={promos}
renderItem={_renderItem}
autoplay={true}
autoplayInterval={4000}
onSnapToItem = { (index) => {
clearTimeout(writeDelay)
writeDelay=setTimeout (()=>{
console.log(promos[index].id)
},2000)
}}
onEndReached={_retrieveMore}
/>
Programmatically snap to item on screen focus.
componentDidMount() {
setTimeout(() => this._carousel.snapToItem(0), 1000);
// if above code doesn't work then you can try is for first item
writeDelay=setTimeout (()=>{
console.log(promos[0].id); // index here is 0 for first item
},1000)
}
...
render() {
<Carousel
...
ref={c => { this._carousel = c }}
/>
}`
you can use this library #svanboxel/visibility-sensor-react-native

You specified `onScroll` on a <ScrollView> but not `scrollEventThrottle`

I am using Scrollview of react-native in my code.
<ScrollView style={styles.productlist} >
<CarouselComponent images={images}/>
</ScrollView>
Its working fine on an app, with no error, and no warning but in Console I am getting the below message repeatedly:
You specified onScroll on a but not scrollEventThrottle. You will only receive one event. Using 16 you get all the events but be aware that it may cause frame drops, use a bigger number if you don't need as much precision.
Don't know what is missing, I have not passed onScroll props but still getting this message.
Any suggestion...
My Carousel component Code is:
const renderImages = (image, index) => {
return (
<Image
key={index}
style={styles.carouselImage}
source={{ uri: image.large }}
/>
);
}
const CarouselComponent = (props) => {
const { images: { alternate = [] } = {} } = props;
if (alternate.length > 0) {
return (
<Carousel
delay={3000}
autoplay
style={styles.carouselLayout}
bullets
chosenBulletStyle={globalStyle.proDetCarSelectBullet}
bulletStyle={globalStyle.productDetailBullet}
bulletsContainerStyle={globalStyle.proDetCarBullet}
isLooped
currentPage={0}
>
{alternate.map((image, index) => renderImages(image, index))}
</Carousel>
);
}
return null;
}
You are probably using an older version of react-native-looped-carousel. On github there is a issue, which was fixed 27 days ago, describing your problem.
Fix:
Updating to the latest version of react-native-looped-carousel should resolve your issue. As an alternative you can fix the error by manually adding the scrollEventThrottle. See here.
There is nothing wrong in your code, the problem is contained into react-native-looped-carousel as an issue states here: https://github.com/phil-r/react-native-looped-carousel/issues/269
I suggest you to search for another library. React-native's code keep growing in a very fast way and every library should be updated frequently.
Just to learn something new, the scrollEventThrottle prop defines how many times the onScroll event will be fired while you scrolling. The bigger the number is, less times the event will be fired. 16 is most precise value.
Actually, the main ScrollView component needs the following code:
<ScrollView
scrollEventThrottle={16}
~~~

React Native elements that are generated outside the Render function does not catch state changes

I have encountered a strange behavior such that when I generate a view outside the Render function, even though it is rendered at the first generation (with the help of this.forceUpdate), it is not re-rendered when the state changes. Which means this does not work properly:
generateTestView() {
let l_generatedView = (
<View
ref={ref => {
this.testAnimatedView = ref;
}}
style={styles.viewOutRender}>
<Text>{'Generated Out of Render - ' + this.state.controlText}</Text>
</View>
);
this.setState({
generatedView: l_generatedView,
buttonToShow: 'showChange',
});
this.forceUpdate();
}
changeControlText() {
this.setState({
controlText: 'NEW VALUE',
});
// this.forceUpdate() also fails here
}
render() {
let l_inRenderView = (
<View style={styles.viewInRender}>
<Text>{'Generated Inside Render - ' + this.state.controlText}</Text>
</View>
);
return (
<View>
... {this.state.generatedView ? this.state.generatedView : undefined}
</View>
);
}
To cross-check, when I generate a view inside the Render function, that view is re-rendered when the state is changed. Which means this works properly as expected:
changeControlText() {
this.setState({
controlText: 'NEW VALUE',
});
}
render() {
let l_inRenderView = (
<View style={styles.viewInRender}>
<Text>{'Generated Inside Render - ' + this.state.controlText}</Text>
</View>
);
return (
<View>
...
{l_inRenderView}
...
</View>
);
}
Related expo snack that can be tested is here. When you click the button "Generate View", it generates a new view and shown properly. Then when you click "Change Control Text", even though the view (generated in Render function, green background) is re-rendered, the other view (generated outside of Render function, yellow background) is not re-rendered.
Please note that, this is a simplified version of a bulk code that I have which is used for animations. Because of some animation related necessities, I need to generate the views outside of the render function.
So the question is how I can force React-Native to re-render the elements that are generated outside of Render function.
Edit: In the actual code there will be hundreds of objects that need to find their locations dynamically and will have discrete animations. That is why regenerating same objects again and again will cause performance problems. That is why I am trying to re-use already generated objects. And that is the reason I am trying to avoid the Render function. Otherwise garbage collector will be in deep trouble.
What you have done here is passed this view fragment
<View
ref={ref => {
this.testAnimatedView = ref;
}}
style={styles.viewOutRender}>
<Text>{'Generated Out of Render - ' + this.state.controlText}</Text>
</View>
to the render function using the state variable. i.e. this.state.generatedView
When you use this syntax, {this.state.generatedView ? this.state.generatedView : undefined}, you are simply embedding that view portion into the greater render() function. However, the embedded view itself is of the same structure, there is no difference from the last embedded view passed to it. Except this variable,
this.state.controlText
If you make no changes to this variable, the internal renderer of react sees the view tree as being identical to the previous tree, and the reconciliation process decides that there are no updates to be made to the view/user interface. It doesn't matter whether the view components are generated in or outside the render function, all that matters is the content returned from the render() function. (into the react framework renderer)
Excerpt from the forceUpdate() docs
React will still only update the DOM if the markup changes.
But yours's hasn't. In order to see a difference,
this.setState({
controlText: 'NEW VALUE' + this.state.controlText,
});
Set your control text to a different value than the previous one. And since you have added the complexity of having part of your view inside the state variable (for reasons that are beyond my comprehension and against every best practice philosophy), call the method that updates the bit of view in your state again.
this.setState({
controlText: 'NEW VALUE' + this.state.controlText,
}, () => generateTestView())
by passing the method as a parameter to the setState function which ensures that the method is only called when the state has updated.
It seems the thing that I was looking for was setNativeProps. Changing the two functions as below solved the problem. I am posting the answer here for anybody who are stuck with similar cases.
generateTestView() {
let l_generatedView = (
<View
ref={ref => {
this.testAnimatedView = ref;
}}
style={styles.viewOutRender}>
<TextInput editable={false} ref={ref => (this.testTextRef = ref)} value={'Generated Out of Render - ' + this.state.controlText} />
</View>
);
this.setState({
generatedView: l_generatedView,
buttonToShow: 'showChange',
});
this.forceUpdate();
}
changeControlText() {
this.setState(
{
controlText: 'NEW VALUE',
},
() => {
this.testTextRef.setNativeProps({text: `Modified in changeControlText - ${this.state.controlText}`});
this.forceUpdate();
}
);
}
Working modified expo snack:
https://snack.expo.io/#mehmetkaplan/3e5325
Credits:
https://medium.com/#payalmaniyar/deep-understanding-of-ref-direct-manipulation-in-react-native-e89726ddb78e
https://hackernoon.com/refs-in-react-all-you-need-to-know-fb9c9e2aeb81

React native performance issue

I am using coincap api's to first fetch Data of about 1500+ crypto currency and then Web-socket to update the updated value of crypto Currency.
I a using redux to manage my state here
Inside My componentDidMount(), I am calling a redux action fetchCoin which fetches the value of the coin
componentDidMount() {
this.props.fetchCoin()
}
And then In return I am doing something like this
<FlatList
data={this.state.searchCoin ? displaySearchCrypto : this.props.cryptoLoaded}
renderItem={({ item }) => (
<CoinCard
key={item["short"]}
coinShortName = {item["short"]}
coinName = {item["long"]}
coinPrice = {item["price"].toFixed(2)}
percentChange = {item["perc"].toFixed(2)}
/>
Then I have a web-socket which updates the value of cryptocurrency like this
componentDidUpdate() {
if (this.state.updateCoinData || this.updateCoinData.length < 1 ) {
this.updateCoinData = [...this.props.cryptoLoaded];
this.setState({updateCoinData: true})
}
this.socket.on('trades', (tradeMsg) => {
for (let i=0; i< this.updateCoinData.length; i++) {
if (this.updateCoinData[i]["short"] == tradeMsg.coin ) {
//Search for changed Crypto Value
this.updateCoinData[i]["perc"] = tradeMsg["message"]["msg"]["perc"]
this.updateCoinData[i]["price"] = tradeMsg['message']['msg']['price']
//Update the crypto Value state in Redux
this.props.updateCrypto(this.updateCoinData);
}
}
})
}
Now, While this work, the problem is that this is slowing my app like hell since whenever the socket sends new data, it has to render every component and hence events like touch and search takes lot of time to execute. [Update] It turns out my app is rendering something even If i remove socket connection, check out update 2
[Question:] What should I do so that I can improve the performance of App? (Something like not using state or using DOM to update my app and so on).
[Update 1:] I am using https://github.com/irohitb/Crypto
And these two are js files where all the logic is happening
https://github.com/irohitb/Crypto/blob/master/src/container/cryptoContainer.js
https://github.com/irohitb/Crypto/blob/master/src/components/CoinCard.js
I have also move from map to Flatlist.
[Update: 2] I found that there are endless render happening inside my App which is probably keeping my thread busy (I mean it is endless & unnecessarily passing props). I asked the same question on separate Stackoverflow thread but didn't received a proper response and since it is related to performance, I thought about putting a bounty on it here.
Please check this thread: infinite Render in React
[Answer Update:] While there are many great answers here, Just in case someone wants to understand how it worked, You could probably clone my repository and go back to before this commit. I have linked the commit to the point where my problems was solved (so you might need to go back and see what I was doing wrong). Also, All the answers were very useful and not hard to comprehend so you should definitely go through them.
Each time your component updates it starts a new socket which results in a memory leak and will cause this.props.updateCrypto(updateCoinData); to be called multiple times for the same data. This can be fixed by opening the socket in componentDidMount() and closing it in componentWillUnmount().
You can also buffer multiple record updates and change the FlatList data in one go every couple of seconds.
Edit, working example (App.js):
import React, { Component } from 'react';
import { Text, View, FlatList } from 'react-native';
import SocketIOClient from 'socket.io-client';
type Props = {};
export default class App extends Component<Props> {
constructor(props) {
super(props);
this.currencies = {};
this.state = {
currenciesList: [],
}
}
componentDidMount() {
this.socket = SocketIOClient('https://coincap.io');
this.socket.on('trades', (tradeMsg) => {
const time = new Date();
// Store updates to currencies in an object
this.currencies[tradeMsg.message.msg.short] = {
...tradeMsg.message.msg,
time: time.getHours() + ':' + time.getMinutes() + ':' + time.getSeconds(),
};
// Create a new array from all currencies
this.setState({currenciesList: Object.values(this.currencies)})
});
}
componentWillUnmount() {
this.socket.disconnect();
}
render() {
return (
<FlatList
data={this.state.currenciesList}
extraData={this.state.currenciesList}
keyExtractor={(item) => item.short}
renderItem={({item}) => <View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
<Text style={{flex: 1}}>{item.time}</Text>
<Text style={{flex: 1}}>{item.short}</Text>
<Text style={{flex: 1}}>{item.perc}</Text>
<Text style={{flex: 1}}>{item.price}</Text>
</View>}
/>
);
}
}
There're many standard ways to improve react app performance, the most common:
use usual react optimizations (shouldComponentUpdate, PureComponent - read docs)
use virtual lists (limit visible parts of data)
In this case I would add:
Don't process data before optimizations - f.e. formatting data that didn't changed is at least unnecessary. You can insert intermediate component (optimization layer) that will pass/update formatted data into <CoinCard /> only on 'raw data' change.
You might not need Redux at all (store data in state) when data is used in one place/simple structure. Of course you can use redux for other globally shared app state (f.e. filtering options).
Use <FlatList /> (react-native), search for sth more suitable?
UPDATE
Some code was changed in mean time (repo), at this time (08.09) one issue still exist and probably causing memory leaks.
You're calling this.socket.on on each componentDidUpdate call (wrongly coded conditions) - continuously adding a new handler!
componentDidUpdate() {
// call all ONLY ONCE afer initial data loading
if (!this.state.updateCoinData && !this.props.cryptoLoaded.length) {
this.setState({updateCoinData: true}) // block condition
this.socket.on('trades', (tradeMsg) => {
// slice() is faster, new array instance
// let updateCoinData = [...this.props.cryptoLoaded];
let updateCoinData = this.props.cryptoLoaded.slice();
for (let i=0; i<updateCoinData.length; i++) {
//Search for changed Crypto Value
if (updateCoinData[i]["short"] == tradeMsg.coin ) {
// found, updating from message
updateCoinData[i]["long"] = tradeMsg["message"]["msg"]["long"]
updateCoinData[i]["short"] = tradeMsg["message"]["msg"]["short"]
updateCoinData[i]["perc"] = tradeMsg["message"]["msg"]["perc"]
updateCoinData[i]["mktcap"] = tradeMsg['message']['msg']["mktcap"]
updateCoinData[i]["price"] = tradeMsg['message']['msg']['price']
//Update the crypto Value state in Redux
this.props.updateCrypto(updateCoinData);
// record found and updated, no more looping needed
break;
}
}
})
}
}
Minor errors: initial fetching states set to true in reducers.
Searching for performance issues I would look at <CoinCard />:
make it PureComponent;
increased and decreased aren't required to be saved at state which forces unnecessasry render calls;
I would use update time (not saved in state, just passed as prop in parent and only for updated rows, within updateCoinData in code above) and derive direction (check for 0 and sign only) of difference (already calculated in perc) only for visible items (from render) and only during time limit (difference between render time and data update prop). setTimeout can be used, too.
finally removing componentWillReceiveProps, componentDidUpdate and shouldComponentUpdate should (highly?) improve performance;
Like Bhojendra Rauniyar said, you should use shouldComponentUpdate in CoinCard. You probably also want to change your FlatList, your downsized sample has the FlatList in a ScrollView, this causes the FlatList to fully expand, thus rendering all it's items at once.
class cryptoTicker extends PureComponent {
componentDidMount() {
this.socket = openSocket('https://coincap.io');
this.props.fetchCoin()
this.props.CurrencyRate()
this.socket.on('trades', (tradeMsg) => {
for (let i=0; i< this.updateCoinData.length; i++) {
if (this.updateCoinData[i]["short"] == tradeMsg.coin ) {
//Search for changed Crypto Value
this.updateCoinData["short"] = tradeMsg["message"]["msg"]["short"]
this.updateCoinData[i]["perc"] = tradeMsg["message"]["msg"]["perc"]
this.updateCoinData[i]["price"] = tradeMsg["message"]['msg']['price']
//Update the crypto Value state in Redux
this.props.updateCrypto(this.updateCoinData);
}
}
})
}
componentWillReceiveProps(newProps){
// Fill with redux data once
if (this.updateCoinData.length < 1 && newProps.cryptoLoaded) {
this.updateCoinData = [...newProps.cryptoLoaded];
}
}
render() {
return (
<View style={{height: '100%'}}>
<Header/>
<FlatList
style={{flex:1}}
data={this.props.cryptoLoaded}
keyExtractor={item => item.short}
initialNumToRender={50}
windowSize={21}
removeClippedSubviews={true}
renderItem={({item, index}) => (
<CoinCard
index={index}
{...item}
/>
)}
/>
</View>
)
}
}
class CoinCard extends Component {
shouldComponentUpdate(nextProps) {
return this.props.price !== nextProps.price || this.props.perc !== nextProps.perc
}
render() {
console.log("here: " + this.props.index);
return (
<View>
<Text> {this.props.index} = {this.props.long} </Text>
</View>
)
}
}
When rendering Flatlist you should consider using PureComponent or utilizing shouldComponentUpdate hook to update only if required.
From the doc:
If your application renders long lists of data (hundreds or thousands of rows), we recommended using a technique known as “windowing”. This technique only renders a small subset of your rows at any given time, and can dramatically reduce the time it takes to re-render the components as well as the number of DOM nodes created.
Take a deep drive to this performance guide.
If you still want some advanced look through, then I will recommend you to look into the following threads:
FlatList and VirtualizedList Scroll performance is laggy after 30+ rows
Performance problems with react when using a big list
You should never do API calls in React's componentWillMount() lifecycle method, instead it should be done in componentDidMount().
Check out this very neat article on lifecycle methods and what should be done in which method: https://medium.com/#baphemot/understanding-reactjs-component-life-cycle-823a640b3e8d.
Many will be tempted to use this function in order to send a request to fetch data and expect the data to be available before the initial render is ready. This is not the case — while the request will be initialized before the render, it will not be able to finish before the render is called.
Instead of creating a socket to update the coinData, you might want to make use of redux subscibe/unsubscribe methods.

Resources