REACT NATIVE - this.setState doesn't work inside onChangeText - reactjs

I'm trying to create a conditional onPress here using a gender selection. If the gender is undefined, I can't go on. Quite simple. But I'm a newbie and I'm not finding the trouble here.
That's the gender prop in state...
constructor(props) {
super(props);
this.state = {
gender: ''
};
That's the const genderSelect with these 3 gender values...
render() {
const genderSelected = [
{ value: 'Feminino', label: 'Feminino' },
{ value: 'Masculino', label: 'Masculino' },
{ value: 'Outro', label: 'Outro' }
];
...
}
That's my Dropdown class where I call the genderSelect and try to use the setState (but it doesn't work, the 'console.log' keeps returning 'undefined'). And finally, that's my conditional onPress.
return (
<>
<Dropdown
label='Selecione...'
data={genderSelect}
baseColor={"white"}
textColor={"white"}
itemColor={"white"}
pickerStyle={{
backgroundColor: "black"
}}
onChangeText={(value) => {
this.setState({ gender : value })
console.log(this.gender)
}}
/>
<TouchableOpacity
style={styles.buttonContainer}
onPress= {()=>{
if (this.gender != undefined ) {
console.log(this.gender)
this.props.navigation.navigate("Abas")
}
else {
console.log(this.gender)
Alert.alert('Ops!','Favor preencher sua idade e gênero antes!');
}
}}
>
<Text style={styles.buttonText}>CONFIRMAR</Text>
</TouchableOpacity>
Btw, I suppose the problem ocurrs inside the onChangeText / setState section. No matter how I use 'if / else' afterwards, it keeps returning 'undefined'.
It’s probably a simple issue whose cause I didn’t get, I know. And I've searched a lot about it, but none of the answers actually helped me. Hope someone can help me.

Replace this.gender with this.state.gender
That's the error.. gender is a state variable so you have to access it like that.
In your return part change this
<TouchableOpacity
style={styles.buttonContainer}
onPress={() => {
if (this.state.gender != undefined) {
console.log(this.state.gender);
this.props.navigation.navigate('Abas');
} else {
console.log(this.state.gender);
Alert.alert('Ops!', 'Favor preencher sua idade e gênero antes!');
}
}}>
<Text style={styles.buttonText}>CONFIRMAR</Text>
</TouchableOpacity>

Related

Using Hooks API: does React respect setState order?

I have fairly nonexistent knowledge in react but I'm learning as I go. I learned the basics back in school, with class components (classic React), but now I'm delving into the Hooks API (mainly because I find it easier to learn and manage, although there seems to be more tricks involved regarding async behavior). So my question might seem silly.
I found this thread regarding setState behavior on the same topic, but this is regarding class components.
In my current application, I'm trying to set three different states using an event handler. It seems that the last state is set immediately, whereas the other two states remain undefined for a bit before changing to a real value. I'm using React-Native components for mobile development, so you'll see snippets in the code such as <SafeAreaView>.
export default App = () => {
const [ destLong, setDestLong ] = useState();
const [ destLat, setDestLat ] = useState();
const [ startNav, setStartNav ] = useState(false);
const [ locations, setLocations ] = useState([
{
name: 'IKEA',
long: '-74.00653395444186',
lat: '40.68324646680103',
},
{
name: 'JFK Intl. Airport',
long: '-73.78131423688552',
lat: '40.66710279890186',
},
{
name: 'Microcenter',
long: '-74.00516039699959',
lat: '40.67195933297655',
}
]);
const startNavigation = (goinglong, goinglat) => {
setDestLong(goinglong);
setDestLat(goinglat);
setStartNav(true);
}
return (
<SafeAreaView style={styles.container}>
{ startNav ?
<MapView
destLong = {destLong}
destLat = {destLat}
/>
:
<View style={styles.buttonContainer}>
<ScrollView>
{
locations.map((location, i) => {
return(
<Card
style={styles.card}
key={i}
title={ location.name }
iconName="home"
iconType="Entypo"
description={ location.long + ", " + location.lat }
onPress={() => startNavigation(location.long, location.lat)}
/>
);
})
}
</ScrollView>
</View>
}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
buttonContainer: {
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center'
},
logo: {
width: '50%',
height: '50%',
resizeMode: 'contain'
},
card: {
marginBottom: 10,
}
});
This throws an error, because MapView is expecting destLong and destLat to render properly. When I console log inside my startNavigation function, it seems that it immediately updates the state for startNav to true onPress, but destLong and destLat remain undefined for a few cycles before being set.
I've tried a different approach like this:
useEffect(() => {
setStartNav(true);
}, [destLong]);
const startNavigation = (goinglong, goinglat) => {
setDestLong(goinglong);
setDestLat(goinglat);
}
But it just crashes the app (my guess is infinite loop).
I've also tried removing the startNav state altogether and rendering <MapView> on destLong like this
{ destLong ?
<MapView
destLong = {destLong}
destLat = {destLat}
/>
:
<View style={styles.buttonContainer}>
...
</View>
}
But that did not work either.
Which brings me to this question: does the Hooks API respect the order of setState, or is each other carried out asynchronously? From my understanding it's the latter. But then, how do you handle this behavior?
I'm adding my comment here as well since I am unable to add proper formatting to my comment above.
Setting a state via useState is actually asynchronous, or rather the state change is enqueued and it will then return its new value after a re-render. This means that there is no guarantee in what order the states will be set. They will fire in order, but they may not be set in the same order.
You can read more here: https://dev.to/shareef/react-usestate-hook-is-asynchronous-1hia, as well as here https://blog.logrocket.com/a-guide-to-usestate-in-react-ecb9952e406c/#reacthooksupdatestate
In your case I would use useState and useEffect like this:
useEffect(() => {
if(destLong && destLat && !startNav) {
setStartNav(true);
}
}, [destLong, destLat, startNav]);
const startNavigation = (goinglong, goinglat) => {
setDestLong(goinglong);
setDestLat(goinglat);
}
With that said, I think you could further simplify your code by omitting the startNav state altogether and update your conditional render:
{ (destLat && destLong) ?
<MapView
destLong = {destLong}
destLat = {destLat}
/>
:
<View style={styles.buttonContainer}>
...
</View>
}
The above should have the same effect since you have two states that are undefined to begin with, and when they are both defined you want to render something and use their values.
And if you want to display the options again you can set the states to undefined again by doing setDestLat(undefined) and setDestLong(undefined)

React Native rerendering UI after using array.map

I am attempting to have an icon switch its visual when clicked (like a checkbox). Normally in react native I would do something like this:
const [checkbox, setCheckbox] = React.useState(false);
...
<TouchableHighlight underlayColor="transparent" onPress={() => {setCheckbox(!setCheckbox)}}>
{added ? <MaterialIcons name="playlist-add-check" size={40} />
: <MaterialIcons name="playlist-add" size={40} />}
</TouchableHighlight>
However I have made some changes, and now I can't seem to replicate this behavior. I am using AsyncStorage class to storage and get arrays of objects for display. For simplification, in the example below I removed the storage code, and the objects each have an 'id' and an 'added' attribute, which is essentially the boolean value of the checkbox.
I am now attempting to update the icon shown to the user whenever it is pressed. I know the function is being called, but it will not update the icon. I am using array.map to create the list of icons. I created a demo here, and the code is below: https://snack.expo.dev/#figbar/array-map-icon-update
const templateObject = {
id: 0,
added: false,
};
const templateObject2 = {
id: 1,
added: true,
};
export default function App() {
const [savedNumbers, setSavedNumbers] = React.useState([]);
React.useEffect(() => {
setSavedNumbers([templateObject,templateObject2]);
}, []);
const populateSavedNumbers = () =>
savedNumbers.map((num, index) => <View key={index}>{renderPanel(num.id,num.added)}</View>);
const updateNumber = (id) => {
let capturedIndex = -1;
for(var i = 0; i < savedNumbers.length; i += 1) {
if(savedNumbers[i].id === id) {
capturedIndex = i;
break;
}
}
let _tempArray = savedNumbers;
_tempArray[capturedIndex].added = !_tempArray[capturedIndex].added;
setSavedNumbers(_tempArray);
}
const renderPanel = (id:number, added:boolean) => {
return (
<View>
<TouchableHighlight underlayColor="transparent" onPress={() => {updateNumber(id);}}>
{added ? <MaterialIcons name="playlist-add-check" size={40} />
: <MaterialIcons name="playlist-add" size={40} />}
</TouchableHighlight>
</View>
);
}
return (
<View>
<View>buttons:</View>
<View>{populateSavedNumbers()}</View>
</View>
);
}
This is a common React pitfall where things don't re-render when it seems like they should. React does shallow comparisons between new and old states to decide whether or not to trigger a re-render. This means that, when declaring a variable to simply equal a state variable which is an object or an array, a re-render is not triggered since those two variables now reference the same underlying data structure.
In this case, you are setting _tempArray to reference the array savedNumbers rather than creating a new array. Therefore, React's shallow comparison comes back as "equal", and it doesn't believe that a re-render is necessary.
To fix this, change this line:
let _tempArray = savedNumbers;
to this:
let _tempArray = [...savedNumbers];

React Native scrollview infinitely rerendering

I'm new to react native and I'm creating a to do list app. the component I wrote keeps infinitely re-rendering when I initialize Todos state to something that isn't an empty array. This results to an error where react stops it. I have no idea what is causing the infinite loop as I haven't included any recursion or explicit loops. I added a few console.log()'s everywhere and found that deleteTodo and toggleTodo keep getting called automatically until the application crashes.
When Todos state is initialized to an empty array I get an error saying that getTodos.map is not a function.
The component is only expected to re-render whenever the state is updated to either modify , add or delete a task.
import React, { useState } from 'react'
import { Platform, StyleSheet, Text, View, TouchableHighlight } from 'react-native'
import { State, ScrollView } from 'react-native-gesture-handler'
/* All required imports are correctly added */
let id = 0
function TaskRow(props) {
return (
<View>
{/* empty button acting as checkbox */}
<TouchableHighlight>
<Text />
</TouchableHighlight>
{/* main text area, displays task and all details + tags*/}
<TouchableHighlight onPress={props.toggle(props.key)}>
<Text>{props.task}</Text>
</TouchableHighlight>
{/* delete button */}
<TouchableHighlight onPress={props.delete(props.key)}>
<Text>Delete</Text>
</TouchableHighlight>
</View>
)
}
export default function TodoList() {
const [
getTodos,
setTodos,
] = useState([
{ key: id, task: 'this is a task', checked: false },
])
const addTodo = (taskInput = 'sample') => {
id++
setTodos(getTodos.push({ key: id, task: 'task', checked: false }))
}
const deleteTodo = (deleteID) => {
setTodos(getTodos.filter((match) => match.key !== deleteID))
}
const toggleTodo = (toggleID) => {
setTodos(
getTodos.map((match) => {
if (match.key == toggleID) return { key: match.key, task: match.task, checked: !match.checked }
}),
)
}
return (
<View>
<TouchableHighlight onPress={addTodo}>
<Text>Add task</Text>
</TouchableHighlight>
<ScrollView>
{getTodos.map((task) => (
<TaskRow
key={task.key}
task={task.task}
delete={deleteTodo}
toggle={toggleTodo}
/>
))}
</ScrollView>
</View>
)
}
You have a problem in your TaskRow component, on each render you are actually calling the functions instead of declaring an handler for events. Please chenge these two lines in this way:
// ...
<TouchableHighlight onPress={() => props.toggle(props.key)}>
// ...
<TouchableHighlight onPress={() => props.delete(props.key)}>
Another problem is this:
setTodos(getTodos.push({ key: id, task: 'task', checked: false }))
the .push() does not return the array so you should do something like:
setTodos([...getTodos, { key: id, task: 'task', checked: false }])

React Native - setNativeProps() on parentElement.props.children = undefined

I'm developing a school management app for myself.
All students in my class are listed in a Flatlist with their parents' phone numbers beside to enable me to send them text messages when a student is absent.
I have a FlatList with Listitems, each of which contains a Touchopacity component with Text child inside.
On successful sending an sms to a student's parent (smsParent method) I want to setNativeProps on both TouchOpacity and its Text child (manipulate their style props). I use ref=(()=>...) to have reference to Touchopacity and then this.props.children (only 1 child) to get to its Text child.
Then however I cannot use setNativeProps (=undefined).
However, when I use ref=(()=>...) on Text as well and then refer to it, setNativeProps works /like in case of its parent/.
Why can't I use setNativeProps() on a child when refering to it by parentEl.props.children? (only 1 child, checked in debugger, it's properly identified)
Please read comments in smsParent method
/*sorry for inserting a snippet - code insertion was crazily formatted/
/**code simplified/
class SingleClassPage extends Component {
buttons = new Array();
constructor(props) {
super(props);
this.state = { students: [] };
this.smsParent = this.smsParent.bind(this);
}
componentDidMount() {
//fetch students from api and setState..
this._getStudentsList();
}
_getStudentsList() {
// ...
}
//flatlist item
ListEl(props) {
return (
<View>
<TouchableOpacity ref={el => { let a = props.item.attId + 'att'; props.buttons[a] = el; }}
style={[styles.buttonDef, (props.item.phone_parent ? styles.buttonBlue : styles.buttonGray)]}
onPress={() => { props.smsSendHandler(props.item, 'attendance', a) }}>
<Text style={props.item.phone_parent ? styles.buttonTextLight : styles.buttonTextDark}>
{props.item.smsAttSent ? 'sms sent' : 'sms send'}
</Text>
</TouchableOpacity>
</View>
)
}
render() {
return (
<View style={{ flex: 1, }}>
<FlatList
data={this.state.students}
extraData={this.state}
keyExtractor={item => item.attId}
renderItem={({ item }) => <this.ListEl buttons={this.buttons} item={item} smsSendHandler={this.smsParent} />}
/>
<BusyIndicator />
</View>
);
}
smsParent(student, msgCategory, smsButton) {
//call myjava module and upon successful callback call below:
let parEl = this.buttons[smsButton];
//childEl is an object with props.children set to text 'sms sent/send' when I watch its value in debugger
//so it's correctly identified
let childEl = parEl.props.children;
// WORKS
parEl.setNativeProps({ style: { backgroundColor: 'green' } });
// OOPS
childEl.setNativeProps({ style: { color: 'black' } });
}
}
edit1
Posting a screenshot of the error (also as response to Dyo's suggestion below - the same error Dyo...)
I think you have to iterate throught children to pass them nativeProps (even if there's only one child) :
smsParent(student, msgCategory, smsButton) {
//call myjava module and upon successful callback call below:
let parEl = this.buttons[smsButton];
React.Children.forEach(parEl.props.children, child => { child.setNativeProps({ style: { color: 'black' } }) });
parEl.setNativeProps({ style: { backgroundColor: 'green' } });
}

How to change icons onPress in React Native

Im new to React Native so excuse the question if this is simple.
Im trying to toggle icons in a ListView. I'm logging out the correct response but Im unable to see the screen refresh onPress with that icon using "this.state". How should I go about this? Your help is much appreciated.
Here's a piece of code i'm working with.
getInitialState: function() {
return {liked:false};
},
pressedLike: function(){
this.setState({liked: !this.state.liked})
},
renderRow: function (data) {
return (
<View>
<TouchableHighlight
onPress={()=>{this.pressedLike}}>
<View>
{this.state.liked ? <Icon name="ios-heart" size={25} color="red" /> : <Icon name="ios-heart-outline" size={25} color="#555" />}
</View>
</TouchableHighlight>
</View>
);
},
render: function() {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderRow}
refreshControl={
<RefreshControl refreshing={this.state.refreshing} onRefresh= {this.onRefresh}/>
}
/>
);
}
It's pretty simple, here is an example of how to change an Awesome icon on press... using the node_module react-native-vector-icon available on GitHub.
First in getInitialState, declare a variable with the icon you want and a check var to know which logo to display:
return {logo: "star-o", check: false}
Then on the concerned button / clickable zone , add this method:
onPress={() => this.stateChange()}
And declare a stateChange method and add this:
this.state.check === false ? this.setState({logo:'star', check:true}) : this.setState({logo:'star-o', check:false})
This looks for the check var and changes the logo depending on its value. It is ternary syntax.
There's really a lot that needs to happen here. You're going to probably need some sort of data structure that has a liked boolean in it, something like this (just an example):
data = [{name: 'Dog', liked: false}, {name: 'Turtle', liked: false}, {name: 'Cat', liked: false}]
Then in your likeItem method, you will need to get the index of the item you are looping over and update the liked boolean, and reset the state of the dataSource:
likeItem (index) {
const { data } = this.state
data[index].liked = !data[index].liked
this.setState({ dataSource: ds.cloneWithRows(data), data })
}
And you can show the icon based on the liked value:
{rowData.liked && <LikedIcon />}
{!rowData.liked && <NotLikedIcon />}
I wipped up a quick example of the functionality you may need here.
https://rnplay.org/apps/H2hsSg

Resources