Updating UI when entry value changes - javascript

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.

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 Native: Counter not showing correct updated state

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.

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 autocomplete component, call endpoint every time a letter is typed

I am working with react and the component autocomplete of material-ui, I need help with the following problem.
In the examples of autocomplete I saw you need to have all elements of the list in the frontend to use the autocomplete, in my case I get the list from a web service and it could be huge, so instead of searching for the whole list I want that every time a letter is typed in the autocomplete it generates a search to the web service filtering names according to the input that is being written and with a max results of 10 elements. The endpoint of the webservice already has a filter property where you can pass the quantity of results you want and the letters you want of the name.The only thing that the autocomplete has to do is everytime you type a letter it hits the endpoint (filtering with the word that is being typed) and updates the list of elements of the autocomplete.
Right now I have the following code, the problem is that it searches the whole list when you click the autocomplete but when you type each letter it doesn't do anything.
import Autocomplete from '#material-ui/lab/Autocomplete';
import TextField from '#material-ui/core/TextField';
import CircularProgress from '#material-ui/core/CircularProgress';
const [open, setOpen] = React.useState(false);
const [organizationList, setOrganizationList] = React.useState([]);
const loading = open && organizationList.length === 0;
React.useEffect(() => {
let active = true;
if (!loading) {
return undefined;
}
(async () => {
if (active) {
try {
setOrganizationList(await api.post('/v1/organizations/search', {maxResults:10}));
} catch (error) {
snackbar.showMessage(error, "error");
}
}
})();
return () => {
active = false;
};
}, [loading]);
React.useEffect(() => {
if (!open) {
setOrganizationList([]);
}
}, [open]);
The definition of the autocomplete:
<Autocomplete
id="asynchronous-demo"
style={{ width: 300 }}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionSelected={(option, value) => option.orgName === value.orgName}
getOptionLabel={(option) => option.orgName}
options={organizationList}
loading={loading}
renderInput={(params) => (
<TextField
{...params}
label="Asynchronous"
variant="outlined"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
To hit the endpoint I have this:
setOrganizationList(await api.post('/v1/organizations/search', {maxResults:10}));
I need to pass the input of the autocomplete every time a letter is typed, like this:
setOrganizationList(await api.post('/v1/organizations/search', {name:inputAutocomplete,maxResults:10}));
Thanks a lot for the help.
Im new to react by the way.
In material-ui library Autocomplete component has a props onChange that can be used like this.
onChange={(event, newValue) => {
setValue(newValue);
}}
You should be interested in the second parameter newValue. Thus, you will receive a new input value every time a letter is typed.
Therefore, just move the logic for getting the list into this callback.
You can read more about controllable state in the material-ui documentation
Implementing the onChange function and giving it the function you already made should give you the solution you want.

React Material UI - Make Slider update state

I am building a multi-step form (survey) with React.js and Material-UI components library.
At the step with a slider, the component doesn't update the state. I was trying to set value with setValue from React.useState() and other various methods. Didn't work out. Does anybody know what the problem is? I'm stuck.
Here is the link to Codesandox: project's code
Code of the Slider component:
import React from 'react';
import { Slider, Input } from '#material-ui/core/';
export default function DiscreteSlider({ values, handleChange }) {
const [value, setValue] = React.useState(values.areaSpace);
const handleSliderChange = (event, newValue) => {
setValue(newValue);
};
const handleInputChange = event => {
setValue(event.target.value === '' ? '' : Number(event.target.value));
};
const handleBlur = () => {
if (value < 0) {
setValue(0);
} else if (value > 100) {
setValue(100);
}
};
return (
<div onChange={handleChange('areaSpace')} style={{marginTop: '20px', marginBottom: '20px'}}>
<Input
value={value}
margin="dense"
onChange={handleInputChange}
onBlur={handleBlur}
inputProps={{
step: 1,
min: 0,
max: 800,
type: 'number',
'aria-labelledby': 'input-slider',
}}
/>
<Slider
style={{marginTop: '20px'}}
value={typeof value === 'number' ? value : 0}
onChange={handleSliderChange}
aria-labelledby="input-slider"
step={1}
min={0}
max={800}
onChangeCommitted={handleChange}
/>
</div>
);
}
On your Slider you have the following:
onChangeCommitted={handleChange}
The handleChange above is being passed from MainForm.js which defines it as:
// Handle inputs change
handleChange = input => event => {
this.setState({ [input]: event.target.value });
}
When that function gets called, all it is going to do is return another function. You need to call handleChange("areaSpace") in order to get a function that will then try to set the "areaSpace" state when it is called. Another problem is that the change function is getting the value from event.target.value, but for the Slider the value is passed as a second parameter.
The following code addresses both of these issues:
onChangeCommitted={(event, value) =>
handleChange("areaSpace")({ target: { value } })
}
There are more elegant ways of dealing with this, but the above fixes the problem without changing any other layers. It does still leave another problem which is that if you change the input instead of the Slider, the areaSpace state won't be updated, but I'll leave that as a problem for you to work through.

Categories