React Navigation routes disappear from history on navigate - reactjs

I have a screen in my app (CameraScreen) that is both in my Tab Navigator and Stack Navigator shown below (there are some more Tab Screens and Stack Screens that I removed for simplicity):
const TabNavigator = () => {
return (
<Tab.Navigator>
<Tab.Screen
name="Camera"
component={CameraScreen}
/>
</Tab.Navigator>
);
};
const Navigation = () => {
return (
<NavigationContainer theme={Theme}>
<Stack.Navigator headerMode="none">
<Stack.Screen name="Camera" component={TabNavigator} />
<Stack.Screen name="Product" component={ProductScreen} />
<Stack.Screen name="CameraStack" component={CameraScreen} />
</Stack.Navigator>
</NavigationContainer>
);
};
Now if I navigate to ProductScreen, then navigate to CameraStack from there and check the navigation state I notice that ProductScreen is nowhere to be found in the routes which I checked using navigation.getState().routes. Thus if I try and use navigation.goBack() it does not go back to the ProductScreen (which would be the expected behaviour).
When I check the routes in ProductScreen, ProductScreen shows up as the last route, however this disappears when I navigate to CameraStack.
I have a hunch that this has to do with the fact that CameraScreen is in both the Tab Navigator and Stack Navigator so for some reason the navigation prop passed to Camera is the Tab Navigator.
For reference my CameraScreen (simplified):
const CameraScreen = ({ navigation, route }) => {
// this doesn't include ProductScreen even if I navigate to CameraStack from the ProductScreen
console.log(navigation.getState().routes);
return (
<View></View>
);
};
and ProductScreen (simplified):
const ProductScreen = ({ navigation }) => {
return (
<View>
<TouchableOpacity
onPress={() => navigation.navigate("CameraStack")}
>
</TouchableOpacity>
</View>
);
};
One idea I can think of to resolve this issue is to manually pass a navigation parameter from ProductScreen but I'm wondering if there is a better way to handle this issue.

I realized this had nothing to do with the fact that I had this screen in both the Tab Navigator and Stack Navigator but was occurring because I was navigating to the same screen using navigation.navigate instead of navigation.push.
The former navigates to the previous location in your stack if you navigate to a screen you've already visited before, but the latter pushes a new route to the navigation state even if it's a screen you've already visited.

Handle backBehavior prop in your Navigator
<Tab.Navigator backBehavior='history'>
<Screen ... />
<Screen ... />
<Tab.Navigator/>
https://reactnavigation.org/docs/bottom-tab-navigator/#backbehavior

Related

React navigation 6 - navigating from one stack A to stack screen B - why screen B wont call useEffect (but renders)?

I have a nested tabs navigator inside a stack navigator.
When I navigate from Tab Screen A to Stack Screen B, B's useEffect is called, all good.
When I navigate from Stack Screen C to also Stack Screen B, B's useEffect is NOT called. I dont understand why.
-I am able to navigate to Screen B from both Screens, thats not the problem. I also am able to pass params. The only thing is just the useEffect is not called when coming from C, coming from A works fine.
I use in both cases same:
<Button title="goTo" onPress={() => props.navigation.navigate('SearchResults', {id: id})}/>
I also tried to use useIsFocused and useFocusEffect, but its also not called when I navigate from stack screen C. What am I doing wrong?
EDIT:
It seems that the problem is with navigating to Screen C (from which I try to navigate to Screen B (that doesnt mount) is that I defined it in the header of Tab Navigator. If I wouldnt, I could navigate to C and C would mount. But I dont understand why that is or what to do.
So In my code : When I navigate from NotificationsScreen to SearchResultsScreen, SearchScreen wont mount (as it seems its because I access NotificationsScreen from header of tabs navigator)
const MyTabs = (props) => {
return (
<Tab.Navigator
screenOptions={{
headerRight: (props) => (
<TouchableOpacity onPress={() => navigation.navigate("Notifications")}>
<Icon name="notifications"/>
</TouchableOpacity>
)
}}
>
<Tab.Screen
name="Home"
component={HomeScreen}
/>
<Tab.Screen
// when I navigate from SearchScreen to SearchResultsScreen, SearchResultsScreen useEffect is called
name="Search"
component={SearchScreen}
/>
</Tab.Navigator>
);
}
const MyStack = () => {
return (
<Stack.Navigator>
<Stack.Screen name="Tabs">
{(props) => <MyTabs {...props} />}
</Stack.Screen>
<Stack.Screen
name="SearchResults"
component={SearchResultsScreen}
/>
<Stack.Screen
name="Notifications"
component={NotificationsScreen}
// when I navigate from NotificationsScreen to SearchResultsScreen, SearchResultsScreen useEffect is NOT called
/>
</Stack.Navigator>
You can use in Notifications component
navigation.replace("SearchResults", null, null)
use **useFocusEffect** on screen B to rerender every time the screen is focused
Here is the link https://reactnavigation.org/docs/use-focus-effect/
import { useFocusEffect } from '#react-navigation/native';
function Profile({ userId }) {
const [user, setUser] = React.useState(null);
useFocusEffect(
React.useCallback(() => {
const unsubscribe = API.subscribe(userId, user => setUser(user));
return () => unsubscribe();
}, [userId])
);
return <ProfileContent user={user} />;
}
because Stack screen B component is mounted when you first navigate to it and it calls useEffect, then you navigate to screen C, here B is not unmounted that's why when you navigate to Screen B again it is already mounted so useEffect does not executes.
you just need to unmount Screen B like following
useEffect(()=>{
//useEffect stuff on mount
return () => {
// on component unmount.
}
},[])

navigation.goBack() not working in nested navigation

my screens are arranged this way:
main root
Stack navigator has 2 screens
Login
Drawer
The drawer is a Drawer Navigator, having three screens
Home
Profile
Settings
Home is a Bottom Tab Navigator having multiple Screens like
Dashboard
Reminder
etc ...
So the issue is whenever I am in any sub-screen of let's say reminder and I want to close it by navigation.goBack() it takes me to Dashboard, what I want is to land back on reminder
PS. the sub screen is also in the tab navigator I have hidden it with filter
Read the following code and try it, don't forget to import it.
The way I've done it is putting the drawer above in the file
DrawerNavigator.js:
const Drawer = createDrawerNavigator()
const DrawerNavigator = () => {
return(
<Drawer.Navigator initialRouteName='TabNavigator'>
<Drawer.Screen name='Home' component={TabNavigator}/>
<Drawer.Screen name='Profile' component={ProfileStackScreen}/>
<Drawer.Screen name='Settings' component={SettingsStackScreen}/>
</Drawer.Navigator>
)
}
export default DrawerNavigator
Here I insert the TabNavigator(Only one of the options in the Drawer will have te bottom tab navigator, the Home one.
The TabNavigator.js will have all the bottom tab screens:
const Tab = createBottomTabNavigator();
const BottomTabNavigator = () => {
return (
<Tab.Navigator initialRouteName='Dashboard'>
<Tab.Screen name="Dashboard" component={DashboarStackScreen} />
<Tab.Screen name='Reminder' component={ReminderStackScreen}/>
</Tab.Navigator>
)
}
export default BottomTabNavigator
In the StackNavigator.js you will enter every stack screens you might want to use:
const Stack = createStackNavigator()
const DashboardStackScreen = () => {
return (
<Stack.Navigator >
{/*INSERT STACK SCREENS HERE*/}
</Stack.Navigator>
)
}
const ReminderStackScreen = () => {
return (
<Stack.Navigator >
{/*INSERT STACK SCREENS HERE*/}
</Stack.Navigator>
)
}
const ProfileStackScreen = () => {
return(
<Stack.Navigator >
{/*INSERT STACK SCREENS HERE*/}
</Stack.Navigator>
)
}
const SettingsStackScreen = () => {
return(
<Stack.Navigator >
{/*INSERT STACK SCREENS HERE*/}
</Stack.Navigator>
)
}
export {DashboardStackScreen,ReminderStackScreen,ProfileStackScreen,SettingsStackScreen}
If this doesn't solve your problem, let me know.
You can use a StackNavigator for each tab of the BottomTabNavigator, which contains the screens for that tab. Every tab will then have its own navigation stack.

React Native - Navigation between nested screens

I have read the documentation regarding navigation between nested screens for react navigation version 5. However, I still keep running into an issue
Below is the code for my App.js
export default function App() {
return (
<NavigationContainer>
<Provider store={store}>
<Stack.Navigator initialRouteName="Login"
headerMode={'none'}>
<Stack.Screen name="Login" component={Login}></Stack.Screen>
<Stack.Screen name="BottomTabs" component={BottomTabs}></Stack.Screen>
</Stack.Navigator>
</Provider>
</NavigationContainer>
);
}
Below is the code for the "BottomTabs" component: -
export default function BottomTabs() {
return (
<Tab.Navigator initialRouteName="Profile">
<Tab.Screen name="Profile"component={Profile}></Tab.Screen>
<Tab.Screen name="Gobble" component={GobbleNavigator}></Tab.Screen>
<Tab.Screen name="Matches" component={Matches}></Tab.Screen>
<Tab.Screen name="Chats" component={ChatRoom}></Tab.Screen>
</Tab.Navigator>
)
}
In the "Profile" screen, I have a sign out button which, when clicked, executes a function where I call props.navigation.navigate('BottomTabs', {screen: 'Login'}) to go to the login page.
I've tried other things too such as navigation.navigate('BottomTabs', {screen: 'App', params: {screen: 'Login'}) and navigation.navigate('Login')
However, when I click the button, nothing happens.
What am I getting wrong here?
There should be a name associated with each screen.
Assuming the Login screen is known as 'Login', then if you want to click a button to navigate to the Login screen, you should be able to do it as:
<Button title="Signout"
onPress={() =>
this.props.navigation.navigate('Login') }
/>
However, if you are doing a sign-out operation, you should first say removing your current logged in data --- before you get navigated to the login screen.

React native navigation mode prop usage

I was looking at react navigation 5 docs and saw the example where you learn to make a full screen modal.
https://reactnavigation.org/docs/modal
In the example above they use a root stack navigator with a nested stack navigator and a screen named Modal, our screen modal, but for some reason mode="modal" prop is used in root stack navigator instead of ModalScreen component (our modal will go from bottom to top as it should, mode prop achieves this) but why is it not used in ModalScreen but in RoosStackNavigator
const MainStack = createStackNavigator();
const RootStack = createStackNavigator();
function RootStackScreen() {
return (
<RootStack.Navigator mode="modal">
<RootStack.Screen
name="Main"
component={MainStackScreen}
options={{ headerShown: false }}
/>
<RootStack.Screen name="MyModal" component={ModalScreen} />
</RootStack.Navigator>
);
}
function MainStackScreen() {
return (
<MainStack.Navigator>
<MainStack.Screen name="Home" component={HomeScreen} />
<MainStack.Screen name="Details" component={DetailsScreen} />
</MainStack.Navigator>
);
}

React Native Navigation override goBack() of header back button in Stack Navigator

I want to customize the behavior of the default back button in stack navigator locally to one screen.
In the details assuming that on the stack there are screen1|screen2, I want to pass some props from screen2 to screen1 once the button is pressed.
I spent a lot of time reading React navigation docs, searching on internet and coding but I am not able to do this.
FROM DOCS
It's possible that in some circumstances that you want to customize the back button more than you can through the options mentioned above, in which case you can set the headerLeft option to a React Element that will be rendered
I know that the issue concerns the goBack() function of the headerRight component.
I want to override the default function goBack() related to the headerLeft back button with something like navigation.navigate("previousScreen",{{..props}}).
And ( this is very important!! ) I want to use this behavior locally to a specific screen, so not globally.
I have tried with something like this but doesn't works.
export default function App(){
return(
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="FirstScreen" component={FirstScreen}/>
<Stack.Screen name="SecondScreen" component={SecondScreen} options={{headerLeft: () => (
<HeaderBackButton
onPress={() =>navigation.navigate("FirstScreen",{//stuff//})}
title="Info"
color="#fff"
/>
),}}/>
</Stack.Navigator>
</NavigationContainer>
)}
If you are using react navigation v5, you can set the behaviour for a specific screen using :
import { HeaderBackButton } from '#react-navigation/stack';
...
options={{
headerLeft: (props) => (
<HeaderBackButton
{...props}
onPress={() => {
navigation.navigate('screenName');
}}
/>
),
}}
...
You can also set as stack level using screenOptions={{}} instead.
"navigation" and "route" are also available on screen props.
options={({ navigation, route }) => ({headerLeft: ...})
For react-navigation v6, you can use setOptions inside a useEffect
import { HeaderBackButton } from '#react-navigation/elements';
...
const MyScreen = ({navigation}) => {
useEffect( () => {
navigation.setOptions({ headerShown: true,
headerLeft: (props) => (
<HeaderBackButton
{...props}
onPress={() => {
navigation.navigate('MyOtherScreen');
}}
/>
)
});
} );
}
When the component is mounted, register back button press listener
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
}
Then in the function, you can check and handle the action, ex:
onBackButtonPressAndroid = () => {
// Check if the current screen is focused or a subscreen is perhaps
if (this.props.navigation.isFocused()) {
if (someCondition) {
// Do a navigation to a custom screen, pass props here if needed
this.props.navigation.navigate('search')
return true
// Return true if you want to tell react navigation you've handled the back press
}
else if (this.state.offsetTop > 10) {
// Ex. for scrolling to top
this.scrollToTop()
this.setState({
offsetTop: 0,
})
return true
}
}
// Return false if you want to tell react navigation you didn't handle the back press
return false
}
Try passing navigationOptions to a specific screen :
export default function App(){
return(
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="FirstScreen" component={FirstScreen}/>
<Stack.Screen name="SecondScreen" component={SecondScreen}
navigationOptions: ({ navigation }) => ({ headerLeft: (<HeaderBackButton onPress={() => {}}/>)
})}}/>
</Stack.Navigator>
</NavigationContainer>
)}
Either you can specify it there on the navigation.
Or on the Second screen, try this :
class SecondScreen extends React.Component {
static navigationOptions = {
headerLeft: (
<Button
onPress={() => alert('This is a back button!')}
title="Title"
color="#fff"
/>
),
};
}

Resources