Access a function which is defined inside a functional component in navigationOptions - react-native

I need to access a function which uses state values. Following is a sample code of my current implementation.
import React, { useState, useEffect } from 'react';
import { View, Text, Button, TouchableOpacity } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { withNavigationFocus } from 'react-navigation';
const HomeScreen = ({ navigation }) => {
const [name, setName] = useState('');
useEffect(() => {
navigation.setParams({
onSave
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onSave]);
const onSave = () => {
// name value will be used in this function
console.log(name);
};
return (
<View>
<Text>{name}</Text>
<Button title="Change name" onPress={() => setName('John')} />
</View>
);
};
HomeScreen.navigationOptions = ({ navigation }) => {
const onSave = navigation.getParam('onSave', false);
return {
title: 'Home',
headerRight: (
<TouchableOpacity onPress={onSave}>
<MaterialCommunityIcons name="content-save" color={'black'} />
</TouchableOpacity>
)
};
};
export default withNavigationFocus(HomeScreen);
Even-though I'm able to access the onSave function. I'm not able to get updated 'name' state. I’m aware that we can reset onSave param on state change, but if there many states needs to be accessed inside onSave function what is the best way to handle this situation?

What if you change the useEffect dependency to 'name' variable:
useEffect(() => {
navigation.setParams({
onSave
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [name]);
Does it make a difference?

Related

Can't understand how to use React Native useEffect

I have a login view in a React Native application:
import React, {useState, useContext, useEffect} from 'react';
import {View, StyleSheet} from 'react-native';
import {Button, TextInput, Headline} from 'react-native-paper';
import globalStyles from '../styles/global';
import axios from 'axios';
import AsyncStorage from '#react-native-community/async-storage';
import AuthContext from '../context/auth/authContext';
const Login = ({navigation, route}) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const {user, setUser} = useContext(AuthContext);
const setLocalStorageUser = async (user) => {
try {
await AsyncStorage.setItem('user', user);
} catch (error) {
console.log(error);
}
};
const handleNewUserPress = () => {
navigation.navigate('Signup');
}
const handleLoginPress = async () => {
try {
const loginData = {
username: email,
password: password,
}
responseData = await axios.post(loginURL, loginData);
setLocalStorageUser('user', {email: email, token: responseData.token});
useEffect(() => {
setUser({email: email, token: responseData.token});
}, []);
} catch (error) {
console.log(error);
}
navigation.navigate('Home');
}
return (
<View style={globalStyles.container}>
<TextInput style={styles.input} value={email} label="Email" onChangeText={(text) => setEmail(text)} />
<TextInput style={styles.input} value={password} label="Contraseña" onChangeText={(text) => setPassword(text)} />
<Button
style={styles.button}
mode='contained'
onPress={() => handleLoginPress()}
disabled={email=='' || password==''}
>
Enviar
</Button>
<Button icon="plus-circle" onPress={() => handleNewUserPress()}>
Nuevo Usuario
</Button>
</View>
);
}
const styles = StyleSheet.create({
input: {
marginBottom: 20,
backgroundColor: 'transparent'
},
button: {
marginBottom: 20
}
})
export default Login;
The problem is in function handleLoginPress()when calls useEffect(). I get this error:
[Error: 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
See url for tips about how to debug and fix this problem.]
I have no idea why it happens and how to solve it.
First, you need to understand, what does the useEffect hook does. According to the documentation:
The Effect Hook lets you perform side effects in function components
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
It was created because we didn't have a way to manage the state inside functional components. We needed to convert the component into a class and use lifecycle methods like: componentDidMount or componentDidUpdate.
In your case, you don't need to use the useEffect hook since your action is being executed when you click the button to login.
You'd like to be using useEffect when:
You need to fetch data
You need to check if the user is logged in
etc...
useEffect is called as a function in the main function before return your jsx, but not inside of the other function or function arrow that you are declaring in the main function. In your case :
import {Button, TextInput, Headline} from 'react-native-paper';
import globalStyles from '../styles/global';
import axios from 'axios';
import AsyncStorage from '#react-native-community/async-storage';
import AuthContext from '../context/auth/authContext';
const Login = ({navigation, route}) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const {user, setUser} = useContext(AuthContext);
useEffect(() => {
setUser({email: email, token: responseData.token});
}, []);
const setLocalStorageUser = async (user) => {
try {
await AsyncStorage.setItem('user', user);
} catch (error) {
console.log(error);
}
};
const handleNewUserPress = () => {
navigation.navigate('Signup');
}
const handleLoginPress = async () => {
try {
const loginData = {
username: email,
password: password,
}
responseData = await axios.post(loginURL, loginData);
setLocalStorageUser('user', {email: email, token: responseData.token});
} catch (error) {
console.log(error);
}
navigation.navigate('Home');
}
return (
<View style={globalStyles.container}>
<TextInput style={styles.input} value={email} label="Email" onChangeText={(text) => setEmail(text)} />
<TextInput style={styles.input} value={password} label="Contraseña" onChangeText={(text) => setPassword(text)} />
<Button
style={styles.button}
mode='contained'
onPress={() => handleLoginPress()}
disabled={email=='' || password==''}
>
Enviar
</Button>
<Button icon="plus-circle" onPress={() => handleNewUserPress()}>
Nuevo Usuario
</Button>
</View>
);
}
const styles = StyleSheet.create({
input: {
marginBottom: 20,
backgroundColor: 'transparent'
},
button: {
marginBottom: 20
}
})
export default Login;
Regards
You don't need useEffect inside handleLoginPress:
const handleLoginPress = async () => {
try {
const loginData = {
username: email,
password: password,
}
responseData = await axios.post(loginURL, loginData);
setLocalStorageUser('user', {email: email, token: responseData.token});
setUser({email: email, token: responseData.token});
} catch (error) {
console.log(error);
}
navigation.navigate('Home');
}
As the error says: You can set useEffect in a function component only, It's not allowed useEffect inside a function expression inside a function component. The best solution, if you want to manage the useEffect you have to handle in the body of your component function and update the state to trigger it.

Change screen without a click event using navigation stack react native

Well what I'm trying to do is when he finishes reading the qr code is to move to the next screen as soon as this event ends. I tried to do this by declaring:
const handleBarCodeScanned = ({ type, data }) => {
{this.props.navigation.navigate ('testScreen', {data1, data2})}
}
Usually, the documentation always shows accompanied by an onClick () function associated with a button.
import React, { useState, useEffect } from 'react';
import { Text, View, StyleSheet, Button, PermissionsAndroid } from 'react-native';
import { BarCodeScanner } from 'expo-barcode-scanner';
import wifi from 'react-native-android-wifi';
export default function QrCodeScreen() {
const [hasPermission, setHasPermission] = useState(null);
const [scanned, setScanned] = useState(false);
useEffect(() => {
(async () => {
const { status } = await BarCodeScanner.requestPermissionsAsync();
setHasPermission(status === 'granted');
})();
}, []);
const handleBarCodeScanned = ({ type, data }) => {
{this.props.navigation.navigate('nextScreen', { data1, data2 })}//Change screen
})}
};
if (hasPermission === null) {
return <Text>Requesting for camera permission</Text>;
}
if (hasPermission === false) {
return <Text>No access to camera</Text>;
}
return (
<View
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-end',
}}>
<BarCodeScanner
onBarCodeScanned={scanned ? undefined : handleBarCodeScanned}
style={StyleSheet.absoluteFillObject}
/>
{scanned && <Button title={'Tap to Scan Again'} onPress={() => setScanned(false)} />}
</View>
);
}
Seems like you're using functional components so there is no this context.
You forget to import and init the navigation hook
import { useNavigation } from '#react-navigation/native';
And
export default function QrCodeScreen() {
const navigation = useNavigation();
...
Then
const handleBarCodeScanned = ({ type, data }) => {
navigation.navigate('nextScreen', { data1, data2 })
})}
I managed to solve the error by passing as the navigation parameter in the function declaration.
Before
export default function QrCodeScreen() {
}
After
export default function QrCodeScreen({navigation}) {
}
Change screen
navigation.navigate('SetupConnectionScreen');

Call onPress method from imported UI navbar component

I am building an app that displays jobs and the commute time for each of them. I have a JobsComponent, where I display the jobs. In this component, I have added an icon on the navbar. When the icon is tapped, a MapComponent should be opened. In this map, I want to display a pin for each job.
The problem that I'm facing is that I've defined the icon in my AppNavigator.js and I want to have the onPress() functionality in JobsComponent.js, but I don't know how to do this. What I've tried:
Adding an ({ onPress }) param to navHeaderRight:
export const navHeaderRight = ({ onPress }) =>
(/UI component goes here/
)
but with no results.
One other idea I had was to define the onPress() behaviour in AppNavigator.js, but this means importing a lot of stuff (array of jobs, details for each job) to AppNavigator.js, which is not a good design decision from my point of view.
I tried just doing a console.log in navHeaderRight.onPress from JobsComponent to see if it works at all. It doesn't.
This is my AppNavigator.js:
import {TouchableHighlight, Image, View} from 'react-native';
import MapComponent from './MapComponent';
export const navHeaderRight = ({ onPress }) =>
(
<View style={{marginRight: 5}}>
<TouchableHighlight underlayColor="transparent">
<Image
source={require('../assets/map.png')}
style={{height: 40, width: 40}}/>
</TouchableHighlight>
</View>
)
const Navigator = createStackNavigator({
Jobs: {
screen: Jobs,
navigationOptions: {
headerRight: navHeaderRight
}
},
MapComponent: {
screen: MapComponent,
navigationOptions: {
header: null
}
}
//other screens defined in the navigator go here
});
const AppNavigator = createAppContainer(Navigator);
export default AppNavigator;
And this is my JobsComponent.js. Here, I try to define the onPress() behaviour in componentDidMount().
import {navHeaderRight} from './AppNavigator';
class Jobs extends Component {
componentDidMount() {
navHeaderRight.onPress = () => {
this.props.navigation.navigate('MapComponent', {/*other params go here*/})
}
}
}
Expected result: when navHeaderRight.onPress is called, the MapComponent should be opened.
**
Actual result: Nothing happens.
**
Any help will be greatly appreciated. :)
You can use React Navigation route parameters to achieve this.
In AppNavigator.js:
import { TouchableHighlight, Image, View } from 'react-native';
import MapComponent from './MapComponent';
const navHeaderRight = (navigation) => {
const handlePress = navigation.getParam('handlePress', null);
return (
<View style={{marginRight: 5}}>
<TouchableHighlight
underlayColor="transparent"
onPress={handlePress}
>
<Image
source={require('../assets/map.png')}
style={{height: 40, width: 40}}
/>
</TouchableHighlight>
</View>
);
}
const Navigator = createStackNavigator({
Jobs: {
screen: Jobs,
navigationOptions: ({ navigation }) => ({
headerRight: navHeaderRight(navigation),
}),
},
MapComponent: {
screen: MapComponent,
navigationOptions: {
header: null,
}
}
});
const AppNavigator = createAppContainer(Navigator);
export default AppNavigator;
In JobsComponent.js:
class Jobs extends Component {
componentDidMount() {
const handlePress = () => console.log('whatever function you want');
this.props.navigation.setParams({ handlePress });
}
// [...]
}

react native Flatlist navigation

I m getting error
TypeError: Cannot read property 'navigation' of undefined. I don't understand how to pass navigation component into each child so when a user presses an item it can navigate to employeeEdit component using React Navigation. i am newbie sorry if this is obvious.
import React, { Component } from 'react';
import { FlatList } from 'react-native';
import { connect } from 'react-redux';
//import { R } from 'ramda';
import _ from 'lodash';
import { employeesFetch } from '../actions';
import { HeaderButton } from './common';
import ListEmployee from './ListEmployee';
class EmployeeList extends Component {
static navigationOptions = ({ navigation }) => ({
headerRight: (
<HeaderButton onPress={() => navigation.navigate('employeeCreate')}>
Add
</HeaderButton>
)
});
componentWillMount() {
this.props.employeesFetch();
}
keyExtractor(item) {
return item.uid;
}
renderItem({ item }) {
return <ListEmployee employee={item} navigation={this.props.navigation} />;
}
render() {
return (
<FlatList
data={this.props.employees}
renderItem={this.renderItem} // Only for test
keyExtractor={this.keyExtractor}
navigation={this.props.navigation}
/>
);
}
}
const mapStateToProps = (state) => {
const employees = _.map(state.employees, (val, uid) => ({ ...val, uid }));
return { employees };
};
export default connect(mapStateToProps, { employeesFetch })(EmployeeList);
Here's the code for ListEmployee
import React, { Component } from 'react';
import {
Text,
StyleSheet,
TouchableWithoutFeedback,
View
} from 'react-native';
import { CardSection } from './common';
class ListEmployee extends Component {
render() {
const { employee } = this.props;
const { navigate } = this.props.navigation;
const { textStyle } = styles;
const { name } = this.props.employee;
return (
<TouchableWithoutFeedback onPress={() => navigate('employeeEdit', { employee })}>
<View>
<CardSection>
<Text style={textStyle}>{name}</Text>
</CardSection>
</View>
</TouchableWithoutFeedback>
);
}
}
/**
second argument in connect does 2 things. 1. dispatches all actions creators
return action objects to the store to be used by reducers; 2. creates props
of action creators to be used by components
**/
export default ListEmployee;
const styles = StyleSheet.create({
textStyle: {
fontSize: 18,
paddingLeft: 15,
}
});
This is one ES6 common pitfall. Don't worry my friend, you only have to learn it once to avoid them all over again.
Long story short, when you declare a method inside React Component, make it arrow function
So, change from this.
renderItem({ item }) {
to this
renderItem = ({ item }) => {
That should solve your problem, for some inconvenient reason, you can only access "this" if you declare your method as an arrow function, but not with normal declaration.
In your case, since renderItem is not an arrow function, "this" is not referred to the react component, therefore "this.props" is likely to be undefined, that is why it gave you this error Cannot read property 'navigation' of undefined since
this.props.navigation = (undefined).navigation
Inside your renderItem method, you can manage what happens when the user presses one an item of your FlatList:
renderItem({ item }) {
<TouchableOpacity onPress={() => { this.props.navigator.push({id: 'employeeEdit'})}} >
<ListEmployee employee={item} navigation={this.props.navigation} />
</TouchableOpacity>
}
Hope it help you!
A navigation sample
here VendorList is the structure rendered
<FlatList
numColumns={6}
data={state.vendoreList}
keyExtractor={(data) => data.id}
renderItem={({ item }) =>
<TouchableOpacity onPress={() => props.navigation.navigate("Home1")} >
<VendorList item={item} />
</TouchableOpacity>
}
/>
in ListEmployee
const {navigation}= this.props.navigation;
this use
<TouchableWithoutFeedback onPress={() => navigation.navigate('employeeEdit', { employee })}>
just need to modification on those two lines, i make text bold what changes you need to do

React Native React Navigation Header Button Event

Hello I 'm trying to bind a function in my Navigator Right Button,
But It gives error.
This is my code:
import React, { Component } from 'react';
import Icon from 'react-native-vector-icons/FontAwesome';
import Modal from 'react-native-modalbox';
import { StackNavigator } from 'react-navigation';
import {
Text,
View,
Alert,
StyleSheet,
TextInput,
Button,
TouchableHighlight
} from 'react-native';
import NewsTab from './tabs/news-tab';
import CustomTabBar from './tabs/custom-tab-bar';
export default class MainPage extends Component {
constructor(props) {
super(props);
}
alertMe(){
Alert.alert("sss");
}
static navigationOptions = {
title: 'Anasayfa',
headerRight:
(<TouchableHighlight onPress={this.alertMe.bind(this)} >
<Text>asd</Text>
</TouchableHighlight>)
};
render() {
return(
<View>
</View>
);
}
}
And Get error like this:
undefined is not an object (evaluating 'this.alertMe.bind')
When I use this method in render function it is working great but in NavigatonOption I cant get handled it. what can I do for this problem.
You should use this in you navigator function
static navigationOptions = ({ navigation }) => {
const { params = {} } = navigation.state;
return {
title: '[ Admin ]',
headerTitleStyle :{color:'#fff'},
headerStyle: {backgroundColor:'#3c3c3c'},
headerRight: <Icon style={{ marginLeft:15,color:'#fff' }} name={'bars'} size={25} onPress={() => params.handleSave()} />
};
};
use the componentwillmount so that it can represent where you are calling function .
componentDidMount() {
this.props.navigation.setParams({ handleSave: this._saveDetails });
}
and then you can write your logic in the function
_saveDetails() {
**write you logic here for **
}
**no need to bind function if you are using this **
May be same as above ...
class LoginScreen extends React.Component {
static navigationOptions = {
header: ({ state }) => ({
right: <Button title={"Save"} onPress={state.params.showAlert} />
})
};
showAlert() {
Alert.alert('No Internet',
'Check internet connection',
[
{ text: 'OK', onPress: () => console.log('OK Pressed') },
],
{ cancelable: false }
)
}
componentDidMount() {
this.props.navigation.setParams({ showAlert: this.showAlert });
}
render() {
return (
<View />
);
}
}
react-navigation v5 version would be:
export const ClassDetail = ({ navigation }) => {
const handleOnTouchMoreButton = () => {
// ...
};
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={handleOnTouchMoreButton}>
<Icon name="more" />
</TouchableOpacity>
),
});
}, [navigation]);
return (
// ...
)
}
This is for navigation v4. You need to modify navigationoptions outside of the functional component. Your button event needs to pass a param via navigation.
pageName['navigationOptions'] = props => ({
headerRight: ()=>
<TouchableOpacity onPress={() => props.navigation.navigate("pageRoute",
{"openAddPopover": true}) } ><Text>+</Text></TouchableOpacity> })
and then in your functional component, you can use that param to do something like this:
useLayoutEffect(() => {
doSomethingWithYourNewParamter(navigation.getParam("openAddPopover"))
}, [navigation])