React Native: Counter not showing correct updated state - javascript

Im trying to put together a simple counter which changes the quantity of an item. Using Hooks to manage its state, i update the value on screen correctly. However, the value the state holds when i console log is always one less than the value on the screen.
For example:
If starting value is 1, after pressing the plus button, the value on screen changes to 2 but in console the state value is still 1.
Setup of Hooks and functions controlling the count:
//Set Quantity
const [quantity, setQuantity] = useState(1);
//Set Item price
const [itemPrice, setItemPrice] = useState(itemOptions[0].price)
//Counter Control
const [disableMinus, setDisableMinus] = useState(true);
const addQuantity = () => {
if (quantity === 1) {
setQuantity(quantity + 1);
setDisableMinus(false);
console.log(quantity)
} else {
if (quantity > 1){
setQuantity(quantity + 1);
setDisableMinus(false);
console.log(quantity)
}
}
}
const minusQuantity = () => {
if (quantity === 1){
setDisableMinus(true);
} else {
if (quantity > 1) {
setQuantity(quantity - 1);
setDisableMinus(false);
console.log(quantity)
}
}
}
return (
<View style={{flexDirection: 'row', alignItems: 'center', justifyContent: "center", paddingVertical: 20}}>
<TouchableOpacity disabled={disableMinus} onPress={() => minusQuantity()}>
<AntDesign style={{color: '#37BD6B'}} name="minuscircle" size={30}/>
</TouchableOpacity>
<Text style={{paddingHorizontal: 10, fontWeight: '700', fontSize: 30}}>{quantity}</Text>
<TouchableOpacity onPress={() => addQuantity()}>
<AntDesign style={{color: '#37BD6B'}} name="pluscircle" size={30}/>
</TouchableOpacity>
)
Thanks in advance

Setters from useState are async.
You could log it this way
useEffect(()=>{
console.log(quantity)
}, [quantity]
This means: when dependency [quantity] change, execute the function passed as first param
To avoid stale closure (happening when 2 fast clicks or rand randers), you should use setters and passing it a function instead of the "current" value:
setQuantity(prevQuantity => prevQuantity + 1);

Here is another way you might set things up:
const addQuantity = () => {
...
setQuantity(oldQuantity => {
const newQuantity = oldQuantity + 1
console.log(newQuantity) // console.log moved here. Also could be console.log("Incremented quantity to:", quantity)
return newQantity
});
...
}
This approach would allow you to log the new quantity value beore it is set. In this approach, you can perform a specific action (in this case console.log) on a case by case basis.
On the other hand, the useEffect approach in #Poptocrack's answer will track all changes to the quantity state. It has significantly less code and could be more desirable depending on your needs.
Hope you find one that fits your needs best from the two approaches.

Related

Trouble with React Native and Firebase RTDB

I'm using React Native (0.68) and Firebase RTDB (with the SDK, version 9), in Expo.
I have a screen that needs to pull a bunch of data from the RTDB and display it in a Flatlist.
(I initially did this without Flatlist, but initial rendering was a bit on the slow side.)
With Flatlist, initial rendering is super fast, huzzah!
However, I have an infinite loop re-render that I'm having trouble finding and fixing. Here's my code for the screen, which exists within a stack navigator:
export function GroupingsScreen () {
... set up a whole bunch of useState, database references (incl groupsRef) etc ...
onValue(groupsRef, (snapshot) => {
console.log('groups onValue triggered')
let data = snapshot.val();
if (loaded == false) {
console.log('--start processing')
setLoaded(true);
let newObject = []
for (let [thisgrouping, contents] of Object.entries(data)) {
let onegroupingObject = { title: thisgrouping, data: [] }
for (let [name, innerdata] of Object.entries(contents.ingredients)) {
onegroupingObject.data.push({ name: name, sku: innerdata.sku, size: innerdata.size,
quantity: innerdata.quantity,
parent: thisgrouping
})
}
newObject.push(onegroupingObject)
}
console.log('--done processing')
setGroupsArray(newObject)
}
});
.... more stuff excerpted ....
return (
<View style={styles.tile}>
<SectionList
sections={groupsArray}
getItemLayout={getItemLayout}
renderItem={ oneRender }
renderSectionHeader={oneSection}
initialNumToRender={20}
removeClippedSubviews={true}
/>
</View>
)};
I'm using loaded/setLoaded to reduce re-renders, but without that code, RN immediately dumps me out for excessive re-renders. Even with it, I get lots of extra renders.
So...
Can someone point me at what's triggering the rerender? The database is /not/ changing.
Is there a better way to get RTDB info into a Flatlist than the code I've written?
I have some code that actually does change the database. That's triggering a full rerender of the whole Flatlist, which is visibly, painfully slow (probably because parts are actually rendering 10x instead of once). Help?
For completeness, here's the OneItem code, so you can see just how complex my Flatlist items are:
const OneItem = (data) => {
// console.log('got data',data)
return (
<View style={[styles.rowView, { backgroundColor: data.sku?'white': '#cccccc'}]} key={data.name}>
<TouchableOpacity style={styles.nameView} onPress={() => {
navigation.navigate('AddEditItemScreen', {purpose: 'Grouping', itemname: data.name, parent: data.parent, mode: 'fix'})
}}>
<View style={styles.nameView}>
<Text style={styles.itemtext}>{data.name}</Text>
{data.sku? null: <Text>"Tap to add SKU."</Text>}
{data.size?<Text>{data.size} </Text>: <Text>no size</Text>}
</View>
</TouchableOpacity>
<View style={styles.buttonView}>
<Button style={styles.smallButton}
onPress={() => { changeQuant(data.quantity ? data.quantity - 1 : -1, data.parent + '/ingredients/' + data.name) }}
>
{data.quantity > 0 ? <Text style={[styles.buttonText, { fontSize: 20 }]}>-</Text>
:<Image source={Images.trash} style={styles.trashButton} />}</Button>
<Text style={styles.quantitytext}>{data.quantity}</Text>
<Button style={styles.smallButton}
onPress={() => {
changeQuant(data.quantity? data.quantity +1 : 1, data.parent+'/ingredients/'+data.name)}}>
<Text style={[styles.buttonText, {fontSize: 20}]}>+</Text></Button>
</View>
</View>
)
};```
I worked out how to stop the rerender (question #1). So, within my Screen functional component, I needed to make another function, and attach the state hook and useEffect to that. I'm not totally sure I understand why, but it gets rid of extra renders. And it's enough to get #3 to tolerable, although perhaps not perfect.
Here's the new code:
export function GroupingsScreen () {
... lots of stuff omitted ...
function JustTheList() {
const [groupsArray, setGroupsArray] = useState([])
useEffect(() => {
const subscriber = onValue(groupsRef, (snapshot) => {
console.log('groups onValue triggered')
let data = snapshot.val();
let newObject = []
for (let [thisgrouping, contents] of Object.entries(data)) {
let onegroupingObject = { title: thisgrouping, data: [] }
for (let [name, innerdata] of Object.entries(contents.ingredients)) {
onegroupingObject.data.push({ name: name, sku: innerdata.sku, size: innerdata.size,
quantity: innerdata.quantity,
parent: thisgrouping
})
}
newObject.push(onegroupingObject)
}
setGroupsArray(newObject)
})
return () => subscriber();
}, [])
return(
<View style={styles.tile}>
<SectionList
sections={groupsArray}
getItemLayout={getItemLayout}
renderItem={ oneRender }
renderSectionHeader={oneSection}
initialNumToRender={20}
removeClippedSubviews={true}
/>
</View>
)
}
And then what was my return within the main functional screen component became:
return (
<JustTheList />
)
I'm still very interested in ideas for improving this code - am I missing a better way to work with RTDB and Flatlist?

React state hooks array element not updating state

I'm trying to reduce the code and/or optimize the use of React state hooks in a form with the rn-material-ui-textfield module. Normally, for a single text field, you could do something like this
import { OutlinedTextField } from 'rn-material-ui-textfield'
// ...and all the other imports
const example = () => {
let [text, onChangeText] = React.useState('');
let [error, set_error] = React.useState('');
const verify = () => {
if(!text) set_error('Enter a text');
else console.log('Finished');
}
return(
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<OutlinedTextField
onChangeText={onChangeText}
value={text}
onSubmitEditing={verify}
error={error}
/>
</View>
);
}
and it would surely work no problem. But as you keep on adding more and more fields, setting a separate error and text hooks for each of them seem tedious and generates a lot of code. So, in order to prevent this, I tried to write this in a different way
// ...all imports from previous snippet
const example = () => {
let [inputs, set_input_arr] = React.useState(['', '', '', '', '', '']);
let [error, set_error_arr] = React.useState(['', '', '', '', '', '']);
const error_names = ['your name', 'an email ID', 'a password', 'your phone number', "your favourite color", 'your nickname'];
const set_input = (index, text) => {
let temp = inputs;
temp[index] = text;
set_input_arr(temp);
};
const set_error = (index, err) => {
let temp = error;
temp[index] = err;
set_error_arr(temp);
// this logs the array correctly after running the loop each time
console.log(`This was set as error: ${error}`);
};
const verify = () => {
for (let i = 0; i < inputs.length; i++) {
if (!inputs[i]) set_error(i, `Please enter ${error_names[i]}`);
}
};
return(
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<OutlinedTextField
onChangeText={text => set_input(0, text)}
value={inputs[0]}
error={error[0]}
/>
<OutlinedTextField
onChangeText={text => set_input(1, text)}
value={inputs[1]}
error={error[1]}
/>
<OutlinedTextField
onChangeText={text => set_input(2, text)}
value={inputs[2]}
error={error[2]}
/>
<OutlinedTextField
onChangeText={text => set_input(3, text)}
value={inputs[3]}
error={error[3]}
/>
<OutlinedTextField
onChangeText={text => set_input(4, text)}
value={inputs[4]}
error={error[4]}
/>
<OutlinedTextField
onChangeText={text => set_input(5, text)}
value={inputs[5]}
error={error[5]}
onSubmitEditing={verify}
/>
<Button onPress={verify} title='Verify' />
</View>
);
}
and it doesn't work. To be clear, the console.log() in the set_error() does print the out as I expected. It adds all the values to the array and prints out the complete array. But then, the state in the elements doesn't change. I strongly believe that this has got something to do with React's way of handling hooks rather than a bug in the <OutlinedTextField /> or something. That's why I'm leaving this here.
If such an approach is impossible with React, please suggest another way to efficiently write code to declare and use these many textfields without declaring all these error hooks.
To fix this change set_error_arr(temp); to set_error_arr([...temp]);.
The reason React does not trigger a re-render when you write set_error_arr(temp); is because of how JS arrays work. temp is holding a reference to the array. This means, that even though the values may be changing the reference has not. Since, the reference has not changed React does not acknowledge a change has occurred to your array. By writing [...temp] you are creating a new array (new reference to point too) thus React acknowledges a change occurred and will trigger a re-render.
This will also occur when working with JS objects
It's because React doesn't think that the Array has changed because it is pointing to the same reference. The content of the array itself has changed, but React only checks if it is the same Array, not the content.
There are two different solution, the first one is to create a new Array with the same content like:
const set_error = (index, err) => {
let temp = [...error];
temp[index] = err;
set_error_arr(temp);
// this logs the array correctly after running the loop each time
console.log(`This was set as error: ${error}`);
};
Or you can checkout the useReducer hook which might be more aligned to what you're trying to implement

React Native - how to re render a component every x milliseconds? "this.setState is not a function?"

Alright, Im trying to update the properties and/or text of a component every x milliseconds. I dove into https://www.npmjs.com/package/react-interval-renderer and the likes, however I got errors pertaining to s.
Im now looking at simply react native (IOS). how to update value date (millisecond) however am having trouble formatting this to my file.
I have:
export default props => {
let [fontsLoaded] = useFonts({
'Inter-SemiBoldItalic': 'https://rsms.me/inter/font-files/Inter-SemiBoldItalic.otf?v=3.12',
});
this.state ={
color: "#fff",
colorAnimTime: 36000,
time: 0
}
setInterval(() => {
this.setState({
time: new Date().getMilliseconds()
});
}, 1000);
//------------------------------------------------------------------->
if (!fontsLoaded) {
return <AppLoading />;
} else {
const styles = StyleSheet.create({
container: { flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
textWrapper: {
height: hp('70%'), // 70% of height device screen
width: wp('80%'), // 80% of width device screen
justifyContent: 'center',
alignItems: 'center',
},
myText: {
fontSize: hp('5.5%'), // End result looks like the provided UI mockup
fontFamily: 'SequelSans-BlackDisp'
}
});
return (
<AnimatedBackgroundColorView
color='#00acdd'
delay={0.5}
duration={36000}
initialColor={this.state.color}
style={styles.container}>
<View style={styles.textWrapper}>
<Text style={styles.myText}>COLOR</Text>
</View>
</AnimatedBackgroundColorView>
);
}
The referenced answer uses componentDidMount() { to enclose the setInterval, however I get syntax errors if I put that where I currently have the setInterval
Now Im getting
this.setState is not a function
And have tried binding the setInterval to the right this but I believe Im not understanding how to set up my file.
After 1000 ms I want to change the 'color' of my <AnimatedBackgroundColorView> and just reload the component. (so it animates again - https://www.npmjs.com/package/react-native-animated-background-color-view )
How can I do this?
Your component is written as a functional component, not a class. To create a stateful functional component, you need to use the hook setState. You're getting this error as there's no object property setState on the component or this. You'll also want to use the useEffect hook to set your interval.
https://reactjs.org/docs/hooks-reference.html
import React, { useState } from 'react';
export default props => {
let [fontsLoaded] = useFonts({
'Inter-SemiBoldItalic': 'https://rsms.me/inter/font-files/Inter-SemiBoldItalic.otf?v=3.12',
});
const color = "#fff";
const colorAnimTime = 36000;
const [time, setTime] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date().getMilliseconds));
}, 1000);
return () => clearInterval(interval);
}, []);
//------------------------------------------------------------------->

Is there an issue with my useState? I feel the values are not truing up right in my Array ReactNative

Been so curious about my console.log output.
I have a button, when clicked it should basically add 1 to the use-State(1) I have created but it seems that I am doing something wrong; Please I need help with know if I am managing my State correctly {I am using React HOOKs}
const [procedureCounter, setProcedureCounter] = useState([1]);
const addProcedureHandler = () => {
setProcedureCounter((procedureCounter) => [
...ProcedureCounter,
{
id: procedureCounter.length,
//value: procedureCounter + 1 // value is undefined for some reason so I removed it but still works
},
]);
console.log(ProcedureCounter);
{ procedureCounter.map((item, value) => (
<View style={{ marginVertical: 15 }} key={item.id + 1}>
<ProcedureSteps step={value + 1} /> //This is a TexteInput Form
</View>
))}
<TouchableOpacity onPress={addProcedureHandler}>
ADD Button
</TouchableOpacity>
Once 'ADD Button' is pressed it does not start from 2 as I have 1 set as initialState.
Below is my Terminal Output after clicking or Pressing 'Add Button' 3 Times
Thank you for taking the time to look into this. I really hoped I explained this as best as I can. Thank you again!
First, have a consistent data type in your state so that is will easy mapping over values to create components.
const [procedureCounter, setProcedureCounter] = useState([{
id: 1,
value: 1
}]);
The reason you are getting undefined is you are not accessing the array properly. Check the below snippet to see how to use it. I have also updated the map iteration for component creation.
const [procedureCounter, setProcedureCounter] = useState([{
id: 1,
value: 1
}]);
const addProcedureHandler = () => {
setProcedureCounter((procedureCounter) => [
...procedureCounter,
{
id: procedureCounter[procedureCounter.length -1].id + 1,
value: procedureCounter[procedureCounter.length -1].value + 1
},
]);
console.log(procedureCounter);
{ procedureCounter.map((item) => (
<View style={{ marginVertical: 15 }} key={item.id}>
<ProcedureSteps step={item.value} /> //This is a TexteInput Form
</View>
))}
<TouchableOpacity onPress={addProcedureHandler}>
ADD Button
</TouchableOpacity>

Updating UI when entry value changes

I have a question about updating UI in real-time (when typing). Is this the right approach?
I have this code:
Regex Method:
determineCardBrand(number) {
var re = new RegExp("^4");
var reMC = new RegExp(
"/^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/",
);
if (number.match(re) != null) {
return "Visa";
} else if (number.match(reMC) != null) {
return "MasterCard";
} else {
return "";
}
}
View code snippet:
<View style={{ flexDirection: "row" }}>
<CardLogo
style={styles.cardLogo}
cardType={
cardNumber === ""
? "Inactive"
: this.determineCardBrand(cardNumber)
}
/>
<TextInputMask
underlineColorAndroid={"transparent"}
type={"credit-card"}
maxLength={19}
style={[Styles.paymentCardInput, { flexGrow: 1 }]}
onChangeText={cardNumber => {
this.setState({ cardNumber });
this.determineCardBrand(cardNumber);
}}
value={cardNumber}
returnKeyType="done"
/>
</View>
Where I want to determine if what card number I type is associated to what CC brand (e.g. visa, mastercard, etc..). So as I type a card number, I want the card logo to change based on what I type for the number. CardLogo component has a switch statement that determines if type name is passed is e.g. Visa, then show the Visa logo.
Is this the right approach?
Normally, you can find out from the first four digits (I think) but sometimes just the first one e.g. like the visa card.. always starts with four. I tested out the above but for some reason, it only works for Visa or unknown CC, not Mastercard. I'm not sure if it's because of the Regex or the way I have the code setup above.
You can do one improvement by adding cardType property to state, by doing this you would only need to determine card type once and it will little more readable as below.
<View style={{ flexDirection: "row" }}>
const { cardType, cardNumber } = this.state;
<CardLogo
style={styles.cardLogo}
cardType={cardType}
/>
<TextInputMask
underlineColorAndroid={"transparent"}
type={"credit-card"}
maxLength={19}
style={[Styles.paymentCardInput, { flexGrow: 1 }]}
onChangeText={cardNumber => {
const cardType = this.determineCardBrand(cardNumber);
this.setState({ cardNumber, cardType });
}}
value={cardNumber}
returnKeyType="done"
/>
With regards to determining mastercard, appearantly there is problem in your master card reg ex, below should work, regex taken from regexinfo
determineCardBrand(number) {
const visa = /^4[0-9]{12}(?:[0-9]{3})?$/;
const master = /^(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$/;
if (visa.test(number)) {
return "Visa";
} else if (master.test(number)) {
return "MasterCard";
} else {
return "";
}}
In a functional component, you could use hooks, on compute the card brand in a useEffect, triggered each time card number changes. I think this would be more efficient since setState is asynchronous (and so you should not assume your state is up to date when calling determineCardBrand() right after setState).
[...]
const [cardNumber, setCardNumber] = useState('');
[...]
useEffect(() => {
determineCardBrand();
}, [cardNumber]);
[...]
<TextInputMask
underlineColorAndroid={"transparent"}
type={"credit-card"}
maxLength={19}
style={[Styles.paymentCardInput, { flexGrow: 1 }]}
onChangeText={cardNumber => {
// const cardType = this.determineCardBrand(cardNumber); not needed anymore
this.setCardNumber(cardNumber);
}}
value={cardNumber}
returnKeyType="done"
/>
Computing the card brand is typically a side effect (depends on a state value), so useEffect would be the perfect tool (IMHO :). But you could also store the card brand in state, and setup const [cardBrand, setCardBrand] = useState('').
Note that this is the functional approach, using hooks introduced in 16.8, so this may not be a viable solution for you. But state is still asynchronous in class based component.

Categories