Buttons and touchables on React Native lag with delay - javascript

I'm tying to create a basic e-shop in my application and it works fine. Every item in flatlist has 3 buttons ( TouchableOpacity ) to set quantity.
Those buttons are very slow: 1 second between clock and re-rendering . It looks like a long press but it's a simple click: this is a simple video to show you
And this is the code in detail:
class Shop extends React.Component {
...
selectItem = (item, typeButton) => {
if (item.qte >= 0) {
switch (typeButton) {
case 'plus':
if (parseFloat(item.EshopPrice) <= parseFloat(this.state.score)) {
this.setState({
score: parseFloat(this.state.score).toFixed(2) - parseFloat(item.EshopPrice).toFixed(2),
})
const actionSum = { type: "INCREASE_SUM", value: item }
this.props.dispatch(actionSum)
} else {
this.showToast();
}
break;
case 'minus':
if (this.props.totaleQte > 0) {
item.qte = item.qte - 1
this.setState({
score: Number(parseFloat(item.EshopPrice).toFixed(2)) + this.state.score,
})
const actionSumMoin = { type: "DECREASE_SUM", value: item }
this.props.dispatch(actionSumMoin)
}
break;
case 'plus+':
if (parseFloat(item.EshopPrice) <= parseFloat(this.state.score)) {
item.qte = item.qte + 1
this.setState({
score: parseFloat(this.state.score).toFixed(2) - parseFloat(item.EshopPrice).toFixed(2),
})
const actionSum = { type: "SET_CURRENTSALE", value: item }
this.props.dispatch(actionSum)
} else {
this.showToast();
}
break;
default:
break;
}
}
};
...
render ()
...
return (...)
}
I call this function in a functional component in the same file which is the renderItem of a flatlit :
renderItem = ({ item }) => {
return (
<View style={StylesGift.buttonsContainer}>
{
item.qte === 0 ?
<TouchableOpacity
onPress={() => this.selectItem(item, 'plus+')}>
<Text style={[StylesGift.itemQte, StylesGift.roundItemQte]}>+</Text>
</TouchableOpacity>
:
<View style={StylesGift.buttonsQteContainer}>
<TouchableOpacity onPress={() => this.selectItem(item, 'minus')}>
<Text style={[StylesGift.itemQte, StylesGift.roundItemQte]}>-</Text>
</TouchableOpacity>
<Pressable
onPress={() => this.showModal(true, item)}>
<Text style={StylesGift.itemQte}>{item.qte}</Text>
</Pressable>
<TouchableOpacity onPress={() => this.selectItem(item, 'plus')}>
<Text style={[StylesGift.itemQte, StylesGift.roundItemQte]}>+</Text>
</TouchableOpacity>
</View>
}
</View>
)
}
I think the problem is in the setState({score: ...}) and dispatching action to redux, because when I remove them all or remove one of them, the click becomes very fast and smooth.
This is the treatment on the reducer:
case 'INCREASE_SUM':
const productShopIndex = state.Data.findIndex(item => item.ProductID === action.value.ProductID)
state.Data[productShopIndex].qte = state.Data[productShopIndex].qte + 1
nextState = {
...state,
sum: state.sum + parseFloat(action.value.EshopPrice),
}
return nextState || state
case 'DECREASE_SUM':
nextState = {
...state,
totaleQte: action.value.qte === 0 ? state.totaleQte - 1 : state.totaleQte,
sum: state.sum - parseFloat(action.value.EshopPrice),
}
return nextState || state

Actually the problem is you are directly dispatching the global actions while pressing the buttons. The side effect of this is when you press + or -, the reducer will take some time to do computation and change the state(Thats why theres delay there since the JS thread is blocked). The simplest solution to this is for every counter, make the increment or decrement as local state and inside useEffect(or componentDidUpdate) sync that count with reducer after some debounce. The flow is:
Store the count value in local state inside the counter
Make debounce of like 500ms so that when user presses any button within this time,it will ignore the last count and only update when user leaves counter after the debounce time.
And sync with the global reducer after the debounce.
I came across similar situation recently. So hope this helps.

Related

React-Native Flatlist all Flatlist Rerender on one Item Selection

I'm building a React-Native app and trying to optimize it, i runned into the case of my Flatlist.
So this Flatlist basically renders few elements and each of these elements are selectable.
The issue i'm facing is that selecting one single item rerenders the whole Flatlist, and thus all items it contains.
I've seen a lot of solutions online already, and tried them without any success.
Here is my code :
Class component containing the Flatlist
const keyExtractor = (item) => item.id
export default class OrderedList extends Component {
state = {
selected: null,
}
onPressSelect = (id) => {
console.log(this.state.selected)
if(this.state.selected === id) {
this.setState({ selected: null})
}
else {
this.setState({ selected: id})
}
}
renderItemOrdered = ({item}) => {
const { group, wording, description, id: uniqueID } = item
const { id, name } = group
return (
<CategoryCard
type="ordered"
// item={item}
uniqueID={uniqueID}
groupName={name}
groupID={id}
description={description}
title={wording}
selected={this.state.selected}
onPressSelect={() => this.onPressSelect(item.id)}
/>
)
}
render() {
return (
<FlatList
initialNumToRender={10}
maxToRenderPerBatch={10}
data={this.props.data}
renderItem={this.renderItemOrdered}
keyExtractor={keyExtractor}
extraData={this.state.selected} ---> Tried with and without it
/>
)
}
}
Class component containing the renderItem method
export default class CategoryCard extends Component {
shouldComponentUpdate = (nextProps, nextState) => {
return nextProps.selected !== this.props.selected &&
nextProps.onPressSelect !== this.props.onPressSelect
}
render(){
if(this.props.type === 'ordered') {
return (
<Pressable style={this.props.selected === this.props.uniqueID ? styles.cardContainerSelected : styles.cardContainer} onPressIn={this.props.onPressSelect}>
<View style={[styles.cardHeader, backgroundTitleColor(this.props.groupID)]}>
<Text style={[styles.cardGroupName, textTitleColor(this.props.groupID)]}>{this.props.groupName}</Text>
</View>
<View style={styles.cardContent}>
<Text style={styles.cardTitle}>{this.props.wording}</Text>
<Text style={styles.cardDescription} numberOfLines={3} ellipsizeMode="tail">{this.props.description}</Text>
</View>
</Pressable>
)
}
}
}
What i already tried :
At first my components were functional components so i changed them into class components in order to make things works. Before that, i tried to use React.memo, also to manually add a function areEqual to it, to tell it when it should rerender, depending on props.
It didn't give me what i wanted.
I also tried to put all anonymous functions outside return statements, made use of useCallback, played around the ShouldComponentUpdate (like adding and removing all the props, the onPress prop, selected props)... None of that worked.
I must be missing something somewhere.. If you can help me with it, it would be a big help !

Show updated variable on another screen React Native

I have my ScreenOne.js file where I have a variable called points with a starting value of 0. I also have some functions in the file that add a value of 10 to the points.
The updated points variable is shown on the ScreenOne and it is updating well. I also want to display the updated points variable in a screen called ScreenTwo.js but that seems to be a problem.
I have already tried to store the updated value in a local storage but it returns null. In this case I try to export the updated variable but it return the variable that has the starting value instead of the updated value.
What could be the best way to transfer and display the updated variable in ScreenTwo?
ScreenOne:
const ScreenOne = ({ navigation }) => {
let valuePoints = 0
module.exports.valuePoints = valuePoints;
if (userInput1.search('No') < 0) {
console.log('Wrong answer!')
} else {
valuePoints += 10
}
if (userInput2.search('Yes') < 0) {
console.log('Wrong answer!')
} else {
valuePoints += 10
}
return (
<ScreenContainer>
<ScrollView style={{ flex: 1, width: '100%' }}>
<PointsAmount>
{valuePoints} //Here it displays 20
</PointsAmount>
</ScrollView>
</ScreenContainer>
)
}
export default ScreenOne;
ScreenTwo:
const ScreenTwo = ({ }) => {
const {valuePoints} = require('../screens/ScreenOne');
return (
<PointsContainer>
<ScrollView style={{flex:1, width: '100%'}}>
<TouchableOpacity onPress={() => navigation.navigate('MyTabs1')} >
<ScoreCard header='Points gained' points={valuePoints} goal={100} /> //Here it should display 20 but instead it displays 0
</TouchableOpacity>
</ScrollView>
</PointsContainer>
)
}
export default ScreenTwo;
You should have a look at the docs for the context in react.
You need to create a context that will be wrapping your App.
const [points, setPoints] = useState(0);
<Context.Provider value={{points, setPoints}}>
Your app
</Context.Provider>
Then in your ScreenOne and ScreenTwo you can get and set the value with:
const {
points, setPoints
} = useContext(Context);

How to refer to a single element of a mapped array to render a View

I've got an array of object. I mapped it to render it like a list with a label and an icon button.
I need to show another View below the element of the row when I hit the button.
The array of object is a let variable. I would have like to make it as a state (because of the re-rendering when a function setState is executed), but I need it as a let variable because they are datas coming from a navigation route
My idea was to add a flag in every object of the array to indicate when the View has to be rendered. This would have been false at the beginning and true when I hit the button, but nothing happen. While if don't use a flag when I hit the button, the View is rendered in every row
state={
visible:false
}
getNewView = (item) => {
if (this.state.visible && item.showViewBelow)
return (
<View>
<HiddenViewComponent/>
</View>
)
};
btnHandler = (item) => {
item.showViewBelow = true;
this.setState({visible:true})
}
render(){
const {navigation} = this.props;
const arrayNav = navigation.getParam('data', '');
let array = [
{
id:'0',
label: arrayNav.label1,
showViewBelow: false
},
{
id:'1',
label: arrayNav.label2,
showViewBelow: false
},
{
id:'2',
label: arrayNav.label3,
showViewBelow: false
}
]
return(
<View>
array.map((item, key)=> { return
<View key = {key} style={{marginHorizontal: 25}}>
<Text>{item.label}</Text>
<TouchableOpacity onPress={this.btnHandler(item)}>
<Image source=(blablabla)/>
</TouchableOpacity>
{this.getNewView(item)}
</View>)
)}
</View>
)
}
So, when i click in a row of the list, I want a new view to be showed below THIS row.
I tried to switch to true the flag in one of the object and leave it to false in the others. It works fine on that line! When I click the button in that row, the new view is correctly rendered below that row.
I tried also using a Flatlist and put some extradata with a state variable to be updated with a setState after the return of the new view. (just to try to re-render the page).
Still, I tried to put this.forceUpdate() in the getNewView method. Nothing happen. Nothing is rendered
So I think the problem is I don't re-render the whole screen, because having the object as let variable, this doesn't happen.
Have any ideas?
You can create method in constant utility to define defaultArray like
getDefaultArray(arrayNav){
return [
{
id:'0',
label: arrayNav.label1,
showViewBelow: false
},
{
id:'1',
label: arrayNav.label2,
showViewBelow: false
},
{
id:'2',
label: arrayNav.label3,
showViewBelow: false
}
];
}
In your component code
import Constants from "./Constants"
ComponentDidMount(){
const {navigation} = this.props;
const arrayNav = navigation.getParam('data', '');
this.state={
visible:false,
array = Constants.getDefaultArray(arrayNav);
}
}
getNewView = (item) => {
if (this.state.visible && item.showViewBelow)
return (
<View>
<HiddenViewComponent/>
</View>
)
};
btnHandler = (item, index) => {
const {navigation} = this.props;
const arrayNav = navigation.getParam('data', '');
let newData = Constants.getDefaultArray(arrayNav);
newData[index].showViewBelow = true;
this.setState({visible:true , array:newData})
}
render(){
return(
<View>
this.state.array.map((item, key)=> { return
<View key = {key} style={{marginHorizontal: 25}}>
<Text>{item.label}</Text>
<TouchableOpacity onPress={this.btnHandler(item,key)}>
<Image source=(blablabla)/>
</TouchableOpacity>
{this.getNewView(item)}
</View>)
)}
</View>
)
}

Changing state for pressed item only

In my react-native app I have a function that changes the state of the pressed item from false to true, but the issue is that it's changing the state for all of the items not only the pressed one, i want when i press to change it for the pressed one only inside my FlatList, here is the code:
Initial state:
state={pressed: false}
Function:
changeItem = async item => {this.setState({ pressed: true });};
Rendering item and binding the function:
_renderItem({ item, index }) {
<TouchableOpacity onPress={this.changeItem.bind(this, item)}>
<Text> Click me </Text>
</TouchableOpacity>
);
}
FlatList:
<FlatList data={this.state.items}
keyExtractor={this._keyExtractor.bind(this)}
renderItem={this._renderItem.bind(this)}/>
The problem here is that you have a list of items, but all of then have the same state.
You need a list of items (an array) but you also need an array of the items state.
So instead of state = { pressed: false } you need state = { pressed: []}
And when rendering the items you need to pass the index of the pressed button:
_renderItem({ item, index }) {
return this.state.pressed[index] &&
<TouchableOpacity onPress={this.changeItem.bind(this, item, index)}>
<Text> Click me </Text>
</TouchableOpacity>
}
And Update the state only in the selected index
changeItem = async (item, index) => {
let itensPressed = this.state.pressed
itensPressed[index] = itensPressed[index] ? false : true
this.setState({ pressed: itensPressed })
}
But there is something better than doing this.
I see that you are getting the items from the state so maybe you want to update the items array an not create another variable.
This depends on how is your this.state.items is coming and if you can or can't have the isPressed value in that array.
If you show how your items is coming, I can create a better answer.
If you have multiple button that have its own state. you need to make an array pressed boolean, in state so that each element in an array has its own pressed state.
Do it like this.
import React, {Component} from 'react';
import { FlatList, TouchableOpacity, Text} from 'react-native';
sampleData = [
{key: 0, pressed: false},
{key: 1, pressed: false},
{key: 2, pressed: false}
]
export default class Example extends Component {
state={
data: sampleData
}
componentDidUpdate(){
console.log(this.state.data)
}
changeItem(item)
{
this.setState( (prevState) => {
prevState.data[item.key] = { ...item, pressed: !item.pressed}
return{
...prevState,
data: [...prevState.data]
}
});
};
_keyExtractor = (item, index) => item.key;
_renderItem({ item, index }) {
return(
<TouchableOpacity onPress={this.changeItem.bind(this,item)}>
<Text> Click me </Text>
</TouchableOpacity>
)
}
render() {
const {data} = this.state
return (
<FlatList
data={data}
keyExtractor={this._keyExtractor}
renderItem={this._renderItem.bind(this)}
/>
)
}
}
i just create an array of object contains key and pressed property and passed it to state.data
check console log, to see the output state.

Toggle button not toggling for the first time

My app is simple: one button with one state toggleButton. In constructor toggleButton is set to the default value false. When I press the button, the app will start record some sensors and console.log their data to the chrome debugger screen.
constructor(props) {
super(props);
this.state = {
toggleButton: false
};
}
recordSensors() {
let toggleButton = !this.state.toggleButton;
this.setState({ toggleButton });
if (this.state.toggleButton) {
// start & record some sensors data
} else {
// stop recording
}
}
render() {
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.toggleButton}
onPress={() => this.recordSensors()}
>
<Text style={styles.buttonText}>
{this.state.toggleButton ? 'Stop' : 'Start'}
</Text>
</TouchableOpacity>
<Text>
{this.state.toggleButton ? 'Recording...' : null}
</Text>
</View>
);
}
The weird thing is, the first time I press the button, its text changed to Stop and the Recording... text appeared but the app didn't record sensors data. When I press the button again (the second time), then it now records.
But if I change if (this.state.toggleButton) to if (toggleButton) then it works fine. I can't understand the logic of it anymore. Can you guys help?
You are using
let toggleButton = !this.state.toggleButton;
Where toggleButton has inverse value of this.state.toggleButton
And, say if, this.state.toggleButton is false then toggleButton will have true as its value. So, the condition you are specifying is totally different here
if (this.state.toggleButton) //if(false)
And when you do
if(toggleButton) //if(true)
So, notice that condition when you have this.state.toggleButton as false or vice-versa
Your problem:
onPress={() => this.recordSensors()}
Fix:
onPress={() => this.recordSensors}
Here is your logic:
In your constructor:
toggleButton = false;
In render:
onPress={() => this.recordSensors()}
Which calls:
//currently this.state.toggleButton == false;
let toggleButton = !this.state.toggleButton; // let toggleButton=true;
this.setState({ toggleButton }); // now state.toggleButton = true;
So now when you click your button, you care call recordSensors() for a second time:
//currently this.state.toggleButton == true;
let toggleButton = !this.state.toggleButton; // let toggleButton =false;
this.setState({ toggleButton }); // now state.toggleButton == false;

Categories

Resources