Passing useState update function to child causes undefined function error - android

Using expo and react-native I have created a screen called MainStateScreen this screen essentially has two direct children called OptionsComponent and NavigationButton. OptionsComponent has multiple children that when modified, update the state of the MainStateScreen so we can pass that as a prop to the next screen with the NavigationButton. That's how it's supposed to work at least.
Instead, when I try to modify the OptionsComponent's children and use MainStateScreen's update function I get the following error.
Here is pseudo-code of my component composition:
MainStateScreen.js
const defaultOptionsObject = [
{
name: '',
value: 1,
count: 0,
},
{
name: '',
value: 8,
count: 0,
},
{
name: '',
value: 20,
count: 0,
},
{
name: '',
value: 25,
count: 0,
},
{
name: '',
value: 30,
count: 0,
},
{
name: '',
value: 32,
count: 0,
},
{
name: '',
value: 100,
count: 0,
},
]
const MainStateScreen = () => {
const [options, setOptions] = useState(defaultOptionsObject)
return (
<View>
<ScrollView>
<OptionsComponent options={options} setOptions={setOptions} />
</ScrollView>
<NavigationButton onPress={() => navigation.push('OtherScreen', { options })} />
</View>
)
}
OptionsComponent.js
const SingleOptionComponent = ({ index, option, options, setOptions }) => {
const [stateOption, setStateOption] = useState({ ...option })
const updateCount = val => {
const tempOption = { ...stateOption }
if (val) {
tempOption.count += 1
} else if (tempOption.count !== 0) {
tempOption.count -= 1
}
setStateOption(tempOption)
}
useEffect(() => {
const tempOptions = [ ...options ]
tempOptions[index] = { ...stateOption }
setOptions(tempOptions) // Commenting this out removes the error.
}, [stateOption])
const AddSubtractButton = () => {
return (
<View>
<TouchableHighlight onPress={() => updateCount(true)}>
<Text>Add</Text>
</TouchableHighlight>
<TouchableHighlight onPress={() => updateCount(false)}>
<Text>Subtract</Text>
</TouchableHighlight>
</View>
)
}
return (
<ListItem rightElement={AddSubtractButton} />
)
}
const OptionsComponent = ({ options, setOptions }) => {
return (
<View>
{options.map((option, index) => {
return (
<SingleOptionComponent
key={`${option?.value}-${index}`}
index={index}
option={option}
options={options}
setOptions={setOptions}
/>
)
})}
<View/>
)
}
The exact error is:
TypeError: undefined is not a function (near '...options.map...')
This error is located at:
in OptionsComponent (at MainStateScreen.js)
BUT when I console.log the setOptions function in the useEffect of SingleOptionComponent, Function setOptions is printed in the console.
If I remove the setOptions(tempOptions) call from the useEffect inside the SingleOptionComponent the error goes away but I need something like that to accomplish the pass up to the parent and over to the NavigationButton.
What could I be doing wrong?

A couple things I would suggest that may solve the issue without re-creating it on my machine. Try adding setOptions to the useEffect dependency array. Maybe for some reason it isn't getting an updated setOptions as it is passed down to the grandchild component. You also should pass in options to it as your effect does dependent on when that value changes.
If that doesn't work try using spread operator setOptions({ ... tempOptions}).
If that doesn't work I would suggest handling the setOptions inside the uppermost component and instead passing a callback down to the grandchild rather than the setOptions function. That callback and get the option pressed in the parent and then update it appropriately based off option._id (I would supply a unique _id to each option rather than dynamically calculating it just to be able to do the above.). Also makes code easier to understand what is going on as the parent will handle it's own state that it needs and setting it appropriately.

Related

How to prevent going back in React Native using React Navigation and 'beforeRemove' eventListener with custom modal?

What I want to achieve is straightforward. I want the user to be forced to confirm exiting a tab navigator called 'checkout'.
I read on React Navigation docs about preventing going back about the 'beforeRemove' event which seems neat and the right thing to use.
The problem is that in their example they call Alert in the eventlistener whereas I want to show a custom modal with a yes and no button.
This is React Navigations example code:
function EditText({ navigation }) {
const [text, setText] = React.useState('');
const hasUnsavedChanges = Boolean(text);
React.useEffect(
() =>
navigation.addListener('beforeRemove', (e) => {
if (!hasUnsavedChanges) {
// If we don't have unsaved changes, then we don't need to do anything
return;
}
// Prevent default behavior of leaving the screen
e.preventDefault();
// Prompt the user before leaving the screen
Alert.alert(
'Discard changes?',
'You have unsaved changes. Are you sure to discard them and leave the screen?',
[
{ text: "Don't leave", style: 'cancel', onPress: () => {} },
{
text: 'Discard',
style: 'destructive',
// If the user confirmed, then we dispatch the action we blocked earlier
// This will continue the action that had triggered the removal of the screen
onPress: () => navigation.dispatch(e.data.action),
},
]
);
}),
[navigation, hasUnsavedChanges]
);
return (
<TextInput
value={text}
placeholder="Type something…"
onChangeText={setText}
/>
);
}
This is the code I have tried:
useEffect(() => {
navigation.addListener('beforeRemove', e => {
if (userConfirmedExit) {
navigation.dispatch(e.data.action);
} else {
e.preventDefault();
setShowExitModal(true);
}
});
}, [navigation, userConfirmedExit]);
const handleConfirmExit = () => {
setUserConfirmedExit(true);
navigation.replace('ProfileTab');
};
const handleDeclineExit = () => setShowExitModal(false);
I am bound to use the navigation.dispatch(e.data.action) inside the eventListener but the handleConfirmExit function must live outside of it and I just can't figure out how to use the beforeRemove listener AND showing a custom modal from where I can exit the tab.
The listener is firing when pressing the back button and the modal shows but nothing happens when pressing yes (i.e running the handleConfirmExit function).
I have tried removing dependencies from the useEffect. The one thing that did work, but only on Android was this:
useEffect(() => {
navigation.addListener('beforeRemove', e => {
e.preventDefault();
setShowExitModal(true);
});
}, [navigation]);
const handleConfirmExit = () => {
navigation.removeListener('beforeRemove', () => {});
navigation.replace('ProfileTab');
};
const handleDeclineExit = () => setShowExitModal(false);
On iOS the modal stays onto the next screen for some reason and the culprit I think is the bad implementation of 'beforeRemove' listener in the last example.
Thank you!
I have a simple solution
navigation.addListener('beforeRemove', (e) => {
if (e.data.action.type !="GO_BACK") {
//"GO_BACK" is emitted by hardware button
navigation.dispatch(e.data.action);
} else {
//your code to prevent hardware back button goes here } //
} )
use BackHandler , you can use navigation.goBack() instead of BackHandler.exitApp()
import { BackHandler} from "react-native";
const backAction = () => {
Alert.alert("Discard changes?", "Are you sure you want to exit?", [
{
text: "NO",
onPress: () => null,
style: "cancel"
},
{ text: "YES", onPress: () => BackHandler.exitApp() }
]);
return true;
};
useEffect(() => {
BackHandler.addEventListener("hardwareBackPress", backAction);
return () => {
BackHandler.removeEventListener("hardwareBackPress", backAction);
}
}, []);
This is what I did and it works fine, but I am sure there is a better solution out there.
const [showExitModal, setShowExitModal] = useState(false);
let exitEvent = useRef<
EventArg<
'beforeRemove',
true,
{
action: Readonly<{
type: string;
payload?: object | undefined;
source?: string | undefined;
target?: string | undefined;
}>;
}
>
>();
useEffect(() => {
const unsubscribe = navigation.addListener('beforeRemove', e => {
e.preventDefault();
exitEvent.current = e;
setShowExitModal(true);
});
return unsubscribe;
}, [navigation]);
const handleConfirmExit = () => {
if (exitEvent.current) {
navigation.dispatch(exitEvent.current.data.action);
}
};
In the markup:
{showExitModal && (
<CheckOutExitModal
onYesPress={handleConfirmExit}
/>
)}

React Native - Encountered two children with the same key, `221`

I am trying to fetch data through API. The data is returned in array, i parsed it to JSON and it shows perfectly in Console Log. But i can't show it on screen, the console shows following warning,
Warning: Encountered two children with the same key, 221. Keys should be unique so that components maintain
their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
This is my code:
import React from 'react';
import { Container, Header, Title, Drawer, Content, Button, Left, Right, Body, Text} from 'native-base';
import { Alert, View, TouchableOpacity, SafeAreaView } from 'react-native';
import { MaterialIcons } from '#expo/vector-icons';
import { Ionicons } from '#expo/vector-icons';
import SideBar from './components/SideBar';
import { FlatList } from 'react-native-gesture-handler';
export default class QrScan extends React.Component{
constructor(props) {
super(props)
this.state = {
resourcedata:'',
};
this.resourceAllocationList = this.resourceAllocationList.bind(this);
}
closeDrawer = () => {
this.drawer._root.close();
}
openDrawer = () => {
this.drawer._root.open();
}
resourceAllocationList() {
fetch('https://api.idepoz.com/ncl/api/getResource', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
}).then((response) => response.json())
.then((responseJson) => {
if(responseJson)
{
var jsonData = JSON.stringify(responseJson.data);
var resultdata = JSON.parse(jsonData);
//console.log(resultdata);
this.setState({resourcedata:resultdata});
}
}).catch((error) => {
console.error(error);
});
}
render()
{
const getHeader = () => {
return <Text>{'Resource Allocation'}</Text>;
};
const getFooter = () => {
if (this.state.loading) {
return null;
}
return <Text>{'Loading...'}</Text>;
};
return(
<Drawer
ref={(ref) => { this.drawer = ref; }}
content={<SideBar navigator={this.navigator} closeDrawer={this.closeDrawer} usertoken=
{this.props.navigation.state.params.usertoken} />}
onClose={() => this.closeDrawer()} >
<Container>
<Header>
<Left>
<Button transparent onPress={this.openDrawer.bind(this)}>
<MaterialIcons name="list" size={40} color="#FFFFFF" />
</Button>
</Left>
<Body>
</Body>
<Right>
<Button transparent>
<Ionicons name="search" size={40} color="#FFFFFF" onPress={() =>
Alert.alert('Search Button pressed')} />
</Button>
</Right>
</Header>
<Content>
<SafeAreaView style={{
flexDirection:"row",
justifyContent:'center',
marginTop: 20,
alignItems: 'center',
marginHorizontal: 20,
}}>
<TouchableOpacity onPress={this.resourceAllocationList}>
<Text>Press Here</Text>
<FlatList data={this.state.resourcedata}
renderItem={({ item }) => {
<Text>{ item.id }</Text>
}}
ListHeaderComponent={getHeader}
ListFooterComponent={getFooter}/>
</TouchableOpacity>
</SafeAreaView>
</Content>
</Container>
</Drawer>
);
}
}
Return Data in Console shows like below:
Array [
Object {
"allocation_space": 1,
"created_at": "2021-03-26 15:49:55",
"created_by": 1,
"date": "2021-04-19",
"deleted_at": null,
"deleted_by": null,
"duration": "01:00:00",
"employee_id": 2,
"end_time": "01:05:00",
"id": 73,
"is_active": 1,
"is_payable": 1,
"order_plan_id": 1,
"price": 13,
"resources_allocation_id": 73,
"serviceuser_id": 1,
"start_time": "00:05:00",
"status": "Approved",
"updated_at": "2021-04-19 07:56:08",
"updated_by": 1,
}.........
Can anyone help how to return above data on screen ?
Try adding extraData prop to Flatlist.
"By passing extraData={selected} to FlatList we make sure FlatList itself will re-render when the state changes. Without setting this prop, FlatList would not know it needs to re-render any items because it is a PureComponent and the prop comparison will not show any changes."
https://docs.expo.dev/versions/latest/react-native/flatlist/
Also adding "key" prop to your Flatlist Text element will take away the error if you don't have multiple same ids in your data. In that case you could use index as key, but this is not the best practice.
So changing your flatlist to...
<FlatList
data={this.state.resourcedata}
extraData={this.state.resourcedata}
renderItem={({ item }) => {
<Text key={item.id}>{ item.id }</Text>
}}
ListHeaderComponent={getHeader}
ListFooterComponent={getFooter
/>
Also I have used props like: initialNumToRender={8} windowSize={16} to reduce memory consumption but this depends on your use case and list size
More information about Flatlist props: https://docs.expo.dev/versions/latest/react-native/flatlist/
(This is my first answer to question so be merciful to me)
EDIT
If braces {} are used in renderItem={} there needs to be return ()
keyExtractor={(item, index) => index.toString()}
renderItem={({ item, index }) => {
return (
<Text key={index }>{ item.id }</Text>
);
}}
Otherwise you could use normal braces () in renderItem which would look like...
keyExtractor={(item, index) => index.toString()}
renderItem={({ item, index }) => (
<Text key={index}>{ item.id }</Text>
)
}
I usually use {} and return as then I can even put logic before return () statement like console.log()

Alert.alert not working in React native iOS, but perfectly fine in Android

Please check the code ,
import {
Alert,
} from 'react-native';
checkForSendingOtp = () => {
let hash = 'aBcDeGgd';
Platform.OS === 'android'
? RNOtpVerify.getHash()
.then(text => {
hash = text + ''.replace('[', '');
hash = hash + ''.replace(']', '');
})
.then(() => {
this.sendDataForOtp(hash);
})
.catch(console.log)
: this.sendDataForOtp(hash);
};
sendDataForOtp(hash) {
axios.post(url,{hash:hash}).then(response=>{
Alert.alert(
'Login Failed',
'Multiple Logins Are Found. \n Logout From All Other Devices to Continue.',
[
{
text: 'Proceed ?',
onPress: () => {}
},
{
text: 'No',
onPress: () => {},
},
],
{cancelable: false},
);
});
}
render() {
return (
<Ripple
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
}}
onPress={this.checkForSendingOtp}
/>
)}
This snippet will work fine in android but not showing in iOS. why ?
Nb :- this is the most of the code that I can share right now , Edited the code please check it now and let me know if you have any questions.
I don't exactly know what happened, There were also a model component which I used for showing custom loading, after removing the model component the alerts starts working.
Replace alert code with below
Alert.alert(
'Login Failed',
'Multiple Logins Are Found. \n Logout From All Other Devices to Continue.',
[
{
text: 'Proceed ?',
onPress: () => {}
},
{
text: 'No',
onPress: () => {},
style: 'cancel'
}
],
{ cancelable: false }
);
It's probably related to this issue: https://github.com/facebook/react-native/issues/10471
For some weird reasons, having the Alert called in a "setTimeout" function make it work. But it’s clearly NOT the best way to avoid this problem. You can also try this solution: https://github.com/facebook/react-native/issues/10471#issuecomment-513641325
setTimeout(() => {
Alert.alert(
"title",
"content",
[
{
text: "ok",
onPress: () => {
},
},
],
)
}, 10)
setTimeout(() => {
//TODO use return and setTimeout
return Alert.alert(
i18n.t('notAtYard.alertTitle'),
'Invalid username or password',
[
{
text: 'Ok',
onPress: () => {
setUserPassword('');
setUsername('');
},
}
],
{cancelable: false},
);
}, 100);
So in my case, I was not using any custom Modal because for custom Modal the solution is simple to wait until the Modal is closed but, I was using the react-native Spinner component, and I was hiding it properly using the visible={loading} prop and yes, the issue was also not resolved after setting cancelable: false in the Alert.alert config and when I increased the timeout to 5000 it worked even without defining cancelable: false in the Alert.alert config but this a lot of timeout was not good for UX so I quickly checked out the Spinner component props and I came to know that it does have cancelable?: boolean | undefined prop so now when I used Spinner component like below it worked even without timeout.
<Spinner
visible={loading}
cancelable={true}
/>
TLDR: use cancelable={true} prop with Spinner component.

Assigning an array as a react component state property?

When trying to set a non empty array as a component state property (at initialisation, in the component constructor), like for example:
this.state = { results: ['apple','orange'] } //in the component constructor
this error is returned in the console:
In this environment the sources for assign MUST be an object.This
error is a performance optimization and not spec compliant
If the array is empty though, that works (but as soon as you try to reasign a non empty array, it would bug)
So this works:
this.state = { results: [] } //in the component constructor
until you do
this.setState({results: ['apple','orange'] }) //in a component function
at which point the previous error would be returned...
I don't understand. Aren't javascript arrays supposed to be of the type object too and thus usable as a state property value?
I tested it with the latest react-native version (0.45.1), on an android device.
The state.results object is only used in a listview like this:
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 })
render () {
return (
<ListView
style={{position: 'absolute', width: 100, backgroundColor: 'white', zIndex: 3}}
keyboardShouldPersistTaps='always'
initialListSize={15}
enableEmptySections={true}
dataSource={ds.cloneWithRows(this.state.results)}
renderRow={(rowData, sectionId, rowId, highlightRow) =>
<RowWrapper styles={styles.rowWrapper} >
<TouchableOpacity
activeOpacity={0.5}
onPress={() => {this.onItemPress(this.state.results[rowId])}}
>
<Text style={styles.rowText}>{rowData}</Text>
</TouchableOpacity>
</RowWrapper>
}
/>
)
}
class RowWrapper extends Component {
constructor (props) {
super(props)
this.defaultTransitionDuration = 500
this.state = {
opacity: new Animated.Value(0)
}
}
componentDidMount () {
Animated.timing(this.state.opacity, {
toValue: 1,
duration: this.defaultTransitionDuration
}).start()
}
render () {
return (
<TouchableWithoutFeedback>
<Animated.View style={{...this.props.styles, opacity: this.state.opacity }}>
{this.props.children}
</Animated.View>
</TouchableWithoutFeedback>
)
}
}
You shouldn't mutate the state.
instead of doing
this.setState({results: ['apple','orange'] })
You should be doing
var resultsArr = this.state.results.slice();
var newResults = ['apple','orange'];
resultsArr.concat(newResults); or resultsArr.push(newResults);
this.setState({results: resultsArr })

Search data in ListView react native

I'm currently learning react-native and getting stuck in ListView problems. I want to search data in ListView from a TextInput and i expect that the result is in ListView too.
Here's what i've done so far:
var PageOne = React.createClass({
getInitialState:function(){
return{
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
}),
loaded: false,
colorProps: {
titleColor: 'white',
},
searchText:"",
}
},
componentDidMount() {
this.fetchData();
},
fetchData() {
fetch(REQUEST_URL)
.then((response) => response.json())
.then((responseData) => {
this.setState({
dataSource: this.state.dataSource.cloneWithRows(responseData.movies),
loaded: true,
});
})
.done();
},
setSearchText(action){
let searchText = event.nativeEvent.text;
this.setState({searchText});
/*
*
*
*/
},
render() {
return (
<View style={{ flex: 1 }}>
<ToolbarAndroid
title="Movies"
{...this.state.colorProps}
style={{height:40, backgroundColor:'blue'}}
/>
<TextInput
placeholder="Search movies......."
value={this.state.searchText}
onChange={this.setSearchText.bind(this)} />
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderMovie}
style={styles.listView}
/>
</View>
);
},
renderMovie(movie) {
return (
<TouchableOpacity onPress={this._handlePressList.bind(this, movie)}>
<View style={styles.container}>
<Image
source={{uri: movie.posters.thumbnail}}
style={styles.thumbnail}
/>
<View style={styles.rightContainer}>
<Text style={styles.title}>{movie.title}</Text>
<Text style={styles.year}>{movie.year}</Text>
</View>
</View>
</TouchableOpacity>
);
},
what am i supposed to do next? Please help. Thanks :)
Update! After read the answer from urbancvek, i add function in setSearchText() method like this:
setSearchText(event){
const searchText = event.nativeEvent.text;
moviesLength = this.state.movies.length;
aMovie = this.state.movies;
const filteredMovies = this.state.movies.filter(checkTitle);
console.log("movies: " + JSON.stringify(filteredMovies));
function checkTitle() {
for(i=0;i<moviesLength;i++){
if(aMovie[i].title === searchText){
console.log("found: " + aMovie[i].title);
return aMovie[i];
}
}
}
this.setState({
searchText,
dataSource: this.state.dataSource.cloneWithRows(filteredMovies),
});
},
But it always show me all the movies, not filtered one. any ideas? thanks
In your fetchData method you should probably save responseData to state too. You will then interact with this data each time search field changes.
fetchData() {
fetch(REQUEST_URL)
.then((response) => response.json())
.then((responseData) => {
this.setState({
dataSource: this.state.dataSource.cloneWithRows(responseData.movies),
movies: responseData.movies,
loaded: true,
});
}).done();
},
Now in your setSearchText() method you should include some filter function that will find the movies you want from the movies you saved to state in fetchData().
setSearchText(action){
const searchText = event.nativeEvent.text;
const filteredMovies = this.state.movies.filter(/* awesome filter function */);
this.setState({
searchText,
dataSource: this.state.dataSource.cloneWithRows(filteredMovies);
});
},
Each time you want to update ListView you have to update it's dataSource. Only this way ListView component can realize that the data it's displaying has changed.
Hope I helped.
Searching data in the listview is basically just searching for it in a linked list or an array just take the input and search for it in the datasource or data blob. You can use linear search or binary search whichever you prefer.
The UIExplorer example from Facebook shows how to do this with ListView:
https://github.com/facebook/react-native/tree/master/Examples/UIExplorer

Categories

Resources