I am experiencing a very weird behaviour in my android emulator when using react-navigation v5 in my react native application, which makes me think is a bug. I found out that there is an open issue on the official react-native-navigation page. See here.
In every secondary screen (see UserProfileScreen below), in many react native-elements like TouchableOpacity Button or TextInput the onPress event doesn't work. It only works in the main screen of the navigator. (See HomeScreen below)
Here is an example of how I create my stack navigator:
import {createStackNavigator} from '#react-navigation/stack';
// some imports here
const HomeStackNavigator = createStackNavigator();
export const HomeNavigator = () => {
return (
<HomeStackNavigator.Navigator>
<HomeStackNavigator.Screen name="HomeScreen" component={HomeScreen}/>
<HomeStackNavigator.Screen name="UserProfileScreen" component={UserProfileScreen}/>
<HomeStackNavigator.Screen name="UserSettingsScreen" component={UserSettingsScreen}/>
</HomeStackNavigator.Navigator>
)
};
As a PoC I have the same code in the main and secondary screen:
<TouchableOpacity
onPress={() => Alert.alert("You pressed me!")} >
<Text> touch me</Text>
</TouchableOpacity>
and I see the alert only in the main screen (HomeScreen).
If I make UserProfileScreen as the fist screen in my stack navigator above then it works (the onPress event) fine on this screen but not in the HomeScreen. So it seems that the onPress event is triggered only in the main screen!. On IOS it works fine in all screens. Any idea ? Let me know if you need some more code snippets.
Related
I am building an app that has three tab navigation.
Each tab wraps a WebView and displays the uri webview is directing.
So, I have three WebViews running in an app.
/* each screen wraps a webview */
/screens
-HomeScreen
-ChatScreen
-ProfileScreen
In each screen, I have back button mapped to call webviewRef.current.goBack() just like below.
useEffect(() => {
const handleBackButtonPress = () => {
if (webviewRef.current) {
webviewRef.current.goBack();
return true;
}
return false;
};
BackHandler.addEventListener("hardwareBackPress", handleBackButtonPress);
return () =>
BackHandler.removeEventListener(
"hardwareBackPress",
handleBackButtonPress
);
}, []);
return (
<Screen>
<WebView
source={{
uri: "http://0.0.0.0:3000/",
}}
ref={webviewRef}
onMessage={handleMessage}
injectedJavaScript={script}
/>
</Screen>
);
It works seamlessly in each webviews, but the problem happens when I move on to different screen(or webview). To help you grasp, I will list the steps to generate the problem I faced.
I open the app and view HomeScreen webview.
I look around in HomeScreen webview (I can use back button)
I decide to move to ProfileScreen and touch profile icon in the tab navigation
I am on ProfileScreen webview.
I look around in ProfileScreen webview (I visit links)
I touch hardware back button and it does not work. (to elaborate, it does nothing.)
My guess is:
the cleanup function in useEffect is somehow removing 'hardwareBackPress' event..
advice appreciated.
Some background for context...I'm creating a react-native app that will live in it's own repository. This app will be integrated into other web, iOS, and Android apps. I'm currently working with the Android app. This is an existing native app written in Java.
I've "wrapped" it with a super basic React Native app, following the React Native Docs. For testing purposes, to mimic what will end up being the production app, I have a native activity, MainActivity in the Android app, with a button that opens the second activity, ReactNativeActivity which is loads my React Native app. This is working - the React Native app loads. The React Native App has a button that should navigate to a second screen within the React Native app (using react-navigation). When I press that button, instead of navigating to the next screen in my stack, it instead reloads MainActivity from the native Android app. Running the same thing on web there is no issue with navigation, so I know the screens are hooked up right. I'm not new to React Native, but I am new to integrating with existing Native applications.
Edited to add some code as requested
The Navigator and the button are as follows:
// navigator.tsx
import React from "react";
import { NavigationContainer } from "#react-navigation/native";
import { createNativeStackNavigator } from "#react-navigation/native-stack";
import { HomeScreen } from "../screens/Home";
import Demo from "../screens/Demo";
export type NavigationStackParamList = {
HomeScreen: undefined;
Demo: undefined;
};
const Stack = createNativeStackNavigator<NavigationStackParamList>();
export const Navigation = () => {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen component={Demo} name="Demo" />
<Stack.Screen
component={HomeScreen}
name="HomeScreen"
options={{ title: "Home" }}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
// Demo Screen
const Demo: React.FC<DemoScreenProps> = ({ navigation }) => {
return (
<View style={styles.app}>
<View style={styles.info}>
<Text style={styles.title}>Demo</Text>
<Text>Running on: </Text>
<Text style={styles.platform}>{Platform.OS}</Text>
</View>
<Map />
<Button
onPress={() => navigation.navigate("HomeScreen")}
title="Go to Test Screen"
/>
</View>
);
};
Pressing "Go to Test Screen" above, is what is causing the MainActivity to reload, rather than the correct screen.
I'm trying to make a SignUp/Login screen with a stackNavigator.
But when I try to navigate from the login page to the signup page.
I've tried creating a const that would be the navigate property but I get the same. If I do all my screens in the same file with the stackNavigator I don't get error but this is not really what I want to do.
Here is my Login Screen first (before getting to this screen in index.js I call the Welcome class to check if the user is logged in are not and I return the if he's not
export default class Login extends React.Component{
render(){
return(
<ScrollView style={{padding : 20}}>
<Text style={styles.title}>
Login
</Text>
<TextInput style={styles.input} placeholder="Email Address"/>
<TextInput style={styles.input} placeholder="Password" />
<View style={{margin : 7}}/>
<TouchableOpacity
onPress={() => this.props.navigation.navigate('Signup')}
>
<Text> Don't have an account? Click here</Text>
</TouchableOpacity>
<Button
onPress={this.props.onLoginPress}
title="Log in"
color='grey'
/>
</ScrollView>
);
}
}
My sign up screen is pretty empty it only returns a View with a title as of right now
And finally here's my StackNavigator file
const AppNavigator = createStackNavigator({
Welcome: {
screen : Welcome
} ,
Login: {
screen: Login
},
Signup : {
screen : Signup
},
},
{
initialRouteName: "Welcome"
});
export default createAppContainer(AppNavigator);
Where Welcome is the file that checked if user is logged in are not
The error I get is "error undefined is not an object (evaluating '_this.props.navigation.navigate')." When I click the in Login.js
checked your repo. Your welcome screen first checks if the user is logged in or not and if not logged in, it renders the login screen. You're not navigating from Welcome screen to Login screen, but rendering another Login screen. The problem lies here. I'll give an example:
In your phone, say iPhone X, you downloaded and installed Facebook from the App Store. Then you logged into your account. That means, the Facebook app in your phone is now configured to use your account. Now, if you want to post your photo, you wont be able to do it if you download and install another Facebook app and try to post from there. You will have to use the same old app which is configured to use your account. (Actually you won't be able to download two at a time :P)
In the same way, you should tell the stack navigator to navigate to the stack navigator's login screen, not render another login screen. The new login screen you created won't have the navigation prop, unless you pass one.
But there is another way. You can render the signup screen the same way you render your login screen. Copy this code to your Welcome.js:
import React from 'react';
import { Text, View, Button } from 'react-native';
import LoginScreen from './LoginScreen';
import SignupScreen from './Signup';
import DrawerNavigator from '../navigation/DrawerNavigator';
const notLoggedIn=0, loggedIn=1, signingUp=2;
export default class Welcome extends React.Component{
constructor(props){
super(props);
this.state={
state: notLoggedIn
}
}
render(){
switch (this.state.state){
case notLoggedIn:
return (
<LoginScreen
onLoginPress={()=>this.setState({state:loggedIn})}
onSignUpPress={()=>this.setState({state:signingUp})}
/>
);
case loggedIn:
return <DrawerNavigator />;
case signingUp:
return <SignupScreen />; //Add onBackPress={()=>this.setState({state:notLoggedIn})}} prop here if you want
}
}
}
Then in your Login.js, change onPress prop from this.props.navigation.navigate('Signup') to this.props.onSignUpPress().
You won't need the stack navigator now.(You've your own navigator now) So, instead of using that stack navigator, you can now directly use your Welcome screen. Now you can just delete the stacknavigator.js file.
This should work out, I guess.
Tip: Saw that you have many unused styles and imports in your files. If you're not really going to use them, you can just remove those lines. That's up to you.
EDIT:
To pass a function into a navigator, you can use the screenProps prop.
Edit your Welcome.js:
case loggedIn:
return <DrawerNavigator logout={()=>this.setState({state:notLoggedIn})} />;
DrawerNavigator.js:
...
const DrawerNavigator = createAppContainer(createDrawerNavigator(
...
));
export default class navigator extends React.Component {
render(){
return <DrawerNavigator screenProps={{logout: this.props.logout}} />
}
}
And inside your logout.js, call the function this.props.screenProps.logout() from wherever you want.
This should work.
We are encountering a very bizarre scenario with react-navigation in our React Native application that is only observed on Android (both in the emulator and on physical devices AND for debug builds as well as release builds), but it works fine on iOS.
Context
We have an existing native application, and decided to implement some new screens in React Native as an experiment to see whether it would benefit our development lifecycle.
Our native app has a sidebar menu, and we added a new menu item, that when selected, takes the user into the React Native portion. They can of course navigate back out whenever they want, and later go back into that React Native portion.
Observed problem (Only occurs in Android)
We have identified it relates to the react-navigation library, but we don't know what we're doing wrong.
When the app is first loaded, the user can select the new menu item and the React Native app loads fine, showing its initial route page and with the StackNavigator working fine.
If the user returns to the native portion (either via the back key, or by selecting a different option from the sidebarmenu) and then later opts to return to the React Native portion, then the StackNavigator portion doesn't display. Other React components outside the StackNavigator get rendered. We know it mounts the contained components, as some of them are making API calls and we see those endpoints being queried. It just doesn't render.
Reloading within the emulator will render the app properly again until we navigate out of React Native and then return.
Oddly enough: If we turn on remote JS debugging, it suddenly all works fine.
So our question:
Can anyone spot what we might be missing in how we are using the StackNavigator, that is keeping it from rendering properly? Again: it works fine when the JS debugger is on, making us think that it is not a logic item, but perhaps a timing condition, or some subtle config? Or should we just ditch react-navigation and go to a different navigation library?
Simple reproduction of the issue
Our package.json is:
{
"dependencies": {
"react": "16.0.0",
"react-native": "0.50.4",
"react-navigation": "1.5.2"
}
}
Our React Native entry page (index.js) is:
import * as React from 'react';
import { AppRegistry, Text, View } from 'react-native';
import { StackNavigator } from 'react-navigation';
import TestPage from './TestPage';
AppRegistry.registerComponent('MyApp', () => MyApp);
class MyApp extends React.Component {
public render() {
return (
<View style={{flex:1}}>
<Text>'This text always shows up fine on Android, even on reentry to React application'</Text>
<AccountNavigator />
</View>
);
}
}
const AccountNavigator = StackNavigator(
{
FirstPage: {
screen: TestPage,
navigationOptions: ({ navigation }) => ({
title: 'Test View'
})
},
{
initialRouteName: 'FirstPage'
}
);
The simple test page (TestPage.js) is just:
import * as React from 'react';
import { Text, View } from 'react-native';
export default class TestPage extends React.Component {
render() {
return (
<View style={{flex:1, alignItems: 'center', justifyContent: 'center'}}>
<Text>'If you can read this, then the app is on first load. But return to the native portion and then try to come back to React Native and you will not see me.'</Text>
</View>
);
}
}
Turns out it was a layout setting issue. In our native code, within our React Activity layout XML we had:
<com.facebook.react.ReactRootView
android:id="#+id/ReactRootView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
and the issue was in the "wrap_content" for height which was causing it to render the StackNavigator() as 1 pixel high. No idea why it always happened only on re-entry and not on the first time, nor why the JS debugger would cause the issue to disappear.
Changing layout_height to "match_parent" resolved the issue altogether.
What is the best practice to link the whole area below to another screen?
The whole area marked with orange needs to be clickable and takes to another screen. Should I put this whole area into a view? What is the best practice here? Would appreciate a code example if possible.
Yes, you can wrap that orange area using View component and try to use React Native Router Flux, the most popular and easiest way to navigate the screen/component.
import React, { Component } from 'react';
import { TouchableOpacity, View } from 'react-native';
import { Actions } from 'react-native-router-flux';
...
export default class HelloWorldList extends Component {
...
render() {
return (
<TouchableOpacity onPress={() => Actions.componentkey}>
<View>
...
</View>
</TouchableOpacity>
);
}
}
Moving from one scene (screen) to another is called navigation, there are plenty of (good and bad) navigation components packages/modules out there and most of them have an Example app to test and learn.
I'd recommend React Navigation as it's really well documented and very easy to understand also it's covered in React Native docs as well.
Other packages:
react-native-router-flux
react-native-navigation
native-navigation