React state hooks array element not updating state - javascript

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

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 Updating an Array of JSON objects onChangeText, index undefined

I've got a useState variable that has an array of JSON objects, and I'm trying to get the text fields to dynamically render and update with onChangeText, but I'm having a bit of an issue with this.
When I add console.log(index) to updateHashtags, it says it is undefined, and I can't understand what I'm doing wrong or can do to make this work. In theory, the index should be a static number for each text field. So the first text field would have hashtags[0] as its value and use '0' as the index for updating the state of hashtags.
When I use:
console.log('index',index);
inside of updateHashtags, I get:
index undefined
Here's the code:
const updateHashtags = (text, index) => {
let ht = hashtags;
ht[index].name = text;
setHashtags(ht);
}
const hashtagElement = (
<>
<Text style={styles.plainText} >Set Your Values:</Text>
<Text style={styles.instructionsText} >This is what other users use to search for you.</Text>
{hashtags.map((e,index) =>
<TextInput
placeholder='value'
key={e.name + index}
value={hashtags[index].name}
onChangeText={(text,index) => updateHashtags(text,index)}
style={styles.textInput}
/>
)}
<TouchableOpacity
onPress={() => {
let ht = hashtags;
let newht = {
name: '',
weight: 0,
};
ht[ht.length] = newht;
setHashtags(ht);
}}
>
<Ionicons
name="add-circle-outline"
size={40}
color={'black'}
/>
</TouchableOpacity>
</>
);
Maybe the onChangeText functions doens't get index param.
Try it like this:
// we are getting index from here
{hashtags.map((e,index) =>
<TextInput
placeholder='value'
key={e.name + index}
value={hashtags[index].name}
// so no need of taking index from param here
onChangeText={(text) => updateHashtags(text,index)}
style={styles.textInput}
/>
)}
Also for your updateHashTags functions consider this insted:
const updateHashtags = (text, index) => {
// doing it this way ensures your are editing updated version of state
setHashtags((state) => {
let ht = state;
ht[index].name = text;
return ht;
});
}

Invalid hook call when I'm setting values from numpad using useChange

This is my component:
const pricePicker = ({
step,
precision,
input,
placeholder,
label,
theme,
props,
meta: { touched, error },
...rest
}) => {
/*In the FOLLOWING LINES from "function useChange(e)" to "return [value,change]"
is the error known as Invalid hook.
*/
function useChange(e){
const [value,setValue] = useState(0);
function change(event){
setValue(value => event.target.value);
}
return [value,change];
}
const handleBlur = (e) => {
if (e.target.value === '0') e.target.value = '0'
}
const handleKeypress = (e) => {
const characterCode = e.key
if (characterCode === 'Backspace') return
const characterNumber = Number(characterCode)
if (characterNumber < 0) {
e.preventDefault()
}
}
const myTheme = {
fontFamily: 'Arial',
textAlign: 'center',
header: {
primaryColor: '#263238',
secondaryColor: '#f9f9f9',
highlightColor: '#FFC107',
backgroundColor: '#607D8B',
},
body: {
primaryColor: '#263238',
secondaryColor: '#32a5f2',
highlightColor: '#FFC107',
backgroundColor: '#f9f9f9',
},
panel: {
backgroundColor: '#CFD8DC'
}
};
return(
<div className='form-group'>
<label forname={input.name}>{label}</label> <br />
<NumPad.Number
{...rest}
step={0.1}
precision={2}
placeholder={!input.value ? 'Please, type a number' : input.value}
selected={input.value ? new NumPad.Number(input.value) : null}
onKeyDown={(changedVal) => handleKeypress(changedVal)}
onBlur={(changedVal) => handleBlur(changedVal)}
onChange={(changedVal) => useChange(changedVal)}
className='form-control'
/>
<div className='text-danger' style={{ marginBottom: '20px' }}>
{touched && error}
</div>
</div>
);
};
export default pricePicker;
When I'm executing this block of code:
function useChange(e){
const [value,setValue] = useState(0);
function change(event){
setValue(value => event.target.value);
}
return [value,change];
}
I'm getting the following issue:
Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
I've tried all ways but it seems it's impossible. I never used hooks and previously I post about something similar but unsuccesfully. Previous post talks about useState is inside pricePicker function is neither a funcional component or react hook component when executed previous code lines like this:
const handleChange = (e) =>{
// in the following line is the error.
const[value, setValue] = useState(0);
}
How can I solve this issue? I need to fix it, but how? I've tried all ways but unsuccessfully.
Any one knows how can I fix this issue? It's important.
The error is actually quite simple - hooks can be used only at the top level of functional components. In your concrete example, you cannot use useState inside of function useChange.
Instead, do something like:
const pricePicker = ({/*props go here*/
const [value,setValue] = useState(0);
// handler of input change
const onChange = e => setValue(e.target.value);
// the rest of the code can stay the same
return <div className='form-group'>
<label forname={input.name}>{label}</label> <br />
<NumPad.Number
{...rest}
step={0.1}
precision={2}
placeholder={!input.value ? 'Please, type a number' : input.value}
selected={input.value ? new NumPad.Number(input.value) : null}
onKeyDown={(changedVal) => handleKeypress(changedVal)}
onBlur={(changedVal) => handleBlur(changedVal)}
onChange={onChange} /* Here's we use the onChange handler */
className='form-control'
/>
<div className='text-danger' style={{ marginBottom: '20px' }}>
{touched && error}
</div>
</div>;
}

React Native load data from API using hooks

Im new in ReactNative and I'm trying to take some data from here https://www.dystans.org/route.json?stops=Hamburg|Berlin
When I try console.log results it return full API response. I dont know why in first results.distance works and return distance, but when I'm trying to do it inside FlatList nothing is returned. Sometimes it works when i want to return only item.distance but can't somethnig like <Text>{item.stops[0].nearByCities[0].city}</Text> nowhere in my code also in console. Im getting error:
undefined is not an object (evaluating 'results.stops[0]')
imports...
const NewOrContinueScreen = ({ navigation }) => {
const [searchApi, results, errorMessage] = useDystans();
console.log(results.distance);
return (
<SafeAreaView forceInset={{ top: "always" }}>
<Text h3 style={styles.text}>
Distance: {results.distance}
</Text>
<Spacer />
<FlatList
extraData={true}
data={results}
renderItem={({ item }) => (
<Text>{item.distance}</Text>
// <Text>{item.stops[0].nearByCities[0].city}</Text>
)}
keyExtractor={item => item.distance}
/>
<Spacer />
</SafeAreaView>
);
};
const styles = StyleSheet.create({});
export default NewOrContinueScreen;
And here is my hook code:
import { useEffect, useState } from "react";
import dystans from "../api/dystans";
export default () => {
const [results, setResults] = useState([]);
const [errorMessage, setErrorMessage] = useState("");
const searchApi = async () => {
try {
const response = await dystans.get("route.json?stops=Hamburg|Berlin", {});
setResults(response.data);
} catch (err) {
setErrorMessage("Something went wrong with useDystans");
}
};
useEffect(() => {
searchApi();
}, []);
return [searchApi, results, errorMessage];
};
As the name implies, FlatList is designed to render a list. Your API endpoint returns a JSON Object, not an Array, so there's nothing for the FlatList to iterate. If you want to show all the stops in the list, try passing in the stops list directly.
<FlatList
data={results.stops}
renderItem={({ item }) => (<Text>{item.nearByCities[0].city}</Text>)}
/>
Some side notes: (1) The extraData parameter is used to indicate if the list should re-render when a variable other than data changes. I don't think you need it here at all, but even if you did, passing in true wouldn't have any effect, you need to pass it the name(s) of the variable(s). (2) The keyExtractor parameter is used to key the rendered items from a field inside of them. The stop objects from the API don't have a member called distance so what you had there won't work. From my quick look at the API response, I didn't see any unique IDs for the stops, so you're probably better off letting React key them from the index automatically.

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