Reset stack inside a tab on tab double press using React Navigation 5 - react-navigation

I am wondering how to reset a stack inside a BottomTabNavigator if the Tab is focused and pressed.
This is the code I have so far:
const Stack = createStackNavigator<MainStackParams>()
const BottomTab = createBottomTabNavigator<TabNavigatorParams>()
const navigationRef = React.useRef()
> Blockquote
<NavigationContainer ref={navigationRef}>
<Stack.Navigator mode="modal">
<Stack.Screen
name={MainAppRoute.BOTTOM_TAB_NAVIGATOR}
component={BottomTabNavigator}
/>
...
</Stack.Navigator>
</NavigationContainer>
function BottomTabNavigator() {
return (
<BottomTab.Navigator>
<BottomTab.Screen
name={TabNavigatorRoute.SOME_STACK}
component={SomeStack}
listeners={{tabPress: e => {
// TODO: Reset stack in current tab (unsure how to do this)
}}}
/>
...
</BottomTab.Navigator>
)
}
In the TODO (in the code snippet) the following should probably be done:
Get navigationRef from app NavigationContainer
Check if the selected BottomTab is focused (to determine a double press)
e.preventDefault
reset SomeStack (unsure how to get the navigation object a stack inside a BottomTabNavigator)
Have any one been able to do this yet?
Appreciate all answers :)

Okey, basically you have 2 options to manage this, first is to check navigation state, but as i found out, it works only on IOS device, android does not do anything.
This piece of code navigated to first screen from this stacknavigator
<Tab.Screen name={`DeviceNavigatorTab`} component={DeviceNavigator} options={{
tabBarIcon: ({tintColor}) => <Image source={require('../../images/feather_home-menu.png')} style={{width: 26, height: 26, tintColor}}/>,
}} listeners={({ navigation, route }) => ({
tabPress: e => {
if (route.state && route.state.routeNames.length > 0) {
navigation.navigate('Device')
}
},
})} />
The other solution, and even better, works on android and IOS operating systems
DeviceNavigatorTab this is tab navigator screen name, {screen: 'Device'} is the first stack screen name in sub stack navigator, hope it helps
<Tab.Screen name={`DeviceNavigatorTab`} component={DeviceNavigator} options={{
tabBarIcon: ({tintColor}) => <Image source={require('../../images/feather_home-menu.png')} style={{width: 26, height: 26, tintColor}}/>,
}} listeners={({ navigation }) => ({
tabPress: e => {
navigation.navigate('DeviceNavigatorTab', { screen: 'Device' });
},
})} />
It is not actually resetting the navigators, it just goes to specified screen inside that tab navigator.

Related

How can I navigate to a nested stack screen inside a sibling tab while preserving its initial screen? (React Navigation)

The Code:
I'm using React Navigation 6 with the following hierarchy:
MainTabNavigator
HomeStack
HomeScreen (HomeStack initial screen, contains a "Pay" button)
OtherScreen
MembershipStack
MembershipHomeScreen (MembershipStack initial screen)
PayMembershipScreen (should always navigate back to MembershipHomeScreen)
The App launches into HomeScreen (inside HomeStack), so the MembershipStack tab won't be loaded yet.
There is a "Pay" button inside HomeScreen that redirects to PayMembershipScreen inside MembershipStack.
Code of the HomeScreen with the "Pay" button press handler:
const HomeScreen = ({navigation}) => {
const payPressHandler = useCallback(() => {
// The parent navigator here is MainTabNavigator
navigation.getParent().navigate("MembershipStack", { screen: "PayMembershipScreen" })
}, [navigation])
return (
<TouchableOpacity onPress={payPressHandler}>
<Text>Go to Pay screen!</Text>
</TouchableOpacity>
)
}
The "Pay" button inside HomeScreen does navigate to PayMembershipScreen with this code.
The Problem:
When the MembershipStack tab has not yet been loaded (i.e. when the App just launched), the following happens:
User clicks the "Pay" button on HomeScreen.
App navigates to PayMembershipScreen.
PayMembershipScreen becomes the initial screen on MembershipStack.
User can't go back to MembershipHomeScreen from PayMembershipScreen.
The question is: how can I navigate from HomeScreen directly to PayMembershipScreen and after that be able to go back to MembershipHomeScreen (i.e. have it available as the initial screen in the MembershipStack history)?
What I've tried so far:
1. Setting lazy: false on the MainTabNavigator options.
This approach does make sure that MembershipHomeScreen is always the initial screen on MembershipStack, since all stacks (and their initial screens) will be loaded when the App launches. However this comes with a noticeable drawback in performance, since there's at least 5 tabs in the actual project.
Also, since MembershipHomeScreen is already focused inside MembershipStack, there's a weird MembershipHomeScreen to PayMembershipScreen transition animation when the "Pay" button on HomeScreen is pressed. I just want the user to see a transition from HomeScreen to PayMembershipScreen at most, nothing flashing inbetween.
2. Define a param on MembershipHomeScreen to indicate when I want to redirect to PayMembershipScreen.
On this approach, I'm using a boolean param on MembershipHomeScreen called redirectToPayment:
Code inside MembershipHomeScreen:
const MembershipHomeScreen = ({navigation, route}) => {
const { redirectToPayment = false } = route.params
const redirectIfNeeded = useCallback(() => {
if (redirectToPayment) {
// reset the param
navigation.setParams({ redirectToPayment: false })
// redirect to the desired screen
navigation.navigate("PayMembershipScreen")
}
}, [redirectToPayment, navigation])
// Using layout effect to avoid rendering MembershipHomeScreen when redirecting.
useLayoutEffect(redirectIfNeeded)
return (
<Text>
This is just the membership home screen,
not the target screen of the "Pay" button.
</Text>
)
}
And on the HomeScreen:
const payPressHandler = useCallback(() => {
navigation
.getParent()
.navigate(
"MembershipStack",
{ screen: "MembershipHomeScreen", params: { redirectToPayment: true } }
)
}, [navigation])
The use of React's useLayoutEffect comes with the known drawback of freezing the screen since it will leave any rendering tasks on hold while it's running. I'm able to notice a 2 seconds freeze in the HomeScreen when I press the "Pay" button on a 4GB RAM Moto G7...
...and even after using useLayoutEffect, the MembershipHomeScreen still renders nonetheless to show a transition animation between MembershipHomeScreen and PayMembershipScreen.
Same behavior with useEffect, except it renders the MembershipHomeScreen instead of a 2 seconds freeze.
3. Using React Navigation's dispatch function to customize the route history.
On this approach, I intend to dispatch a custom action that does the following to the navigation state of MainTabNavigator:
Before:
index: 0
routes: [
0: {
name: "HomeStack",
state: {
index: 0,
routes: [
0: {
name: "Home" // <-- currently active screen
}
]
}
},
1: {
name: "MembershipStack",
state: undefined // <-- this is an unloaded tab
}
]
After:
index: 1
routes: [
0: {
name: "HomeStack",
state: {
index: 0,
routes: [
0: {
name: "Home"
}
]
}
},
1: {
name: "MembershipStack",
state: {
index: 1,
routes: [
0: {
name: "MembershipHomeScreen"
},
1: {
name: "PayMembershipScreen" // <-- currently active screen
}
]
}
},
]
Here's the code I'm using inside the HomeScreen for that:
const payPressHandler = useCallback(() => {
navigation.getParent().dispatch(state => {
const membershipTabIndex = state.routes.findIndex(r => r.name === "MembershipStack")
// get the current navigation state inside the MembershipStack
let membershipTabState = state.routes[membershipTabIndex].state
// point to PayMembershipScreen without overriding the initial screen on that tab
if (!membershipTabState) {
// tab is unloaded, so just set the ideal state
membershipTabState = { index: 1, routes: [{ name: "MembershipHomeScreen" }, { name: "PayMembershipScreen" }] }
} else {
// tab already has a navigation state, so we'll point to PayMembershipScreen
// if it's loaded in the stack. Otherwise, we'll add it and point to it.
let payMembershipScreenIndex = membershipTabState.routes.findIndex(r => r.name === "PayMembershipScreen")
if (payMembershipScreenIndex === -1) {
payMembershipScreenIndex = membershipTabState.routes.push({ name: "PayMembershipScreen" }) - 1
}
membershipTabState.index = payMembershipScreenIndex
}
// update the MembershipStack tab with the new state
const routes = state.routes.map((r, i) => i === membershipTabIndex ? { ...r, state: membershipTabState} : r)
// update the MainTabNavigator state
return CommonActions.reset({
...state,
routes,
index: membershipTabIndex
})
})
}, [navigation])
That code almost accomplishes the expected outcome:
It navigates from HomeScreen to PayMembershipScreen successfuly.
The user can go back to MembershipHomeScreen from PayMembershipScreen.
MembershipHomeScreen does not render in-between during the transition.
However:
It won't work a second time if you do go back to MembershipHomeScreen from PayMembershipScreen.
Turns out, if I go back to MembershipHomeScreen once I'm inside PayMembershipScreen, then go back to the HomeStack and press the "Pay" button again, it will now navigate to MembershipHomeScreen instead of PayMembershipScreen.
Additionally, the MembershipHomeScreen will now display a disabled back button in the header (probably a bug).
This last approach is so far the closest to getting the desired outcome, so I really hope that it only needs a fix in the logic and it's not really a bug.
Summary:
Is anyone able find a solution that achieves the expected outcome? To sum up:
It should make the "Pay" button navigate from HomeScreen to PayMembershipScreen.
The user should be able to go back to MembershipHomeScreen once they're in PayMembershipScreen.
The MembershipHomeScreen should not flash in the transition from HomeScreen to PayMembershipScreen.
The screen should not freeze inbetween the transitions (no use of useLayoutEffect).
Don't load all tabs on App launch (it's too much burden on the actual 5-tab project).
If the user navigates back to MembershipHomeScreen once they're inPayMembershipScreen, pressing the "Pay" button on HomeScreen again should open the PayMembershipScreen again (no buggy behavior).
Minimal reproducible example:
Here's a snack with all of the approaches mentioned. Please check it out and use it as a playground for your solution!
https://snack.expo.dev/#ger88555/tabs-with-stacks-and-a-button
Solution
I got the expected outcome with the following approach:
Navigate directly to PayMembershipScreen when the "Pay" button is pressed.
Inside PayMembershipScreen, add MembershipHomeScreen to the top of MembershipStack history if not already present.
The Code
Code inside HomeScreen (no changes):
const HomeScreen = ({navigation}) => {
const payPressHandler = useCallback(() => {
navigation.getParent().navigate("MembershipStack", { screen: "PayMembershipScreen" })
}, [navigation])
return (
<TouchableOpacity onPress={payPressHandler}>
<Text>Go to Pay screen!</Text>
</TouchableOpacity>
)
}
New useAssertInitialScreen hook:
const useAssertInitialScreen = ({ navigation, name }) => {
useEffect(() => {
if (navigation.canGoBack() === false) {
navigation.dispatch((state) => {
const routes = [{ name }, ...state.routes]
return CommonActions.reset({
...state,
routes,
index: state.index + 1
})
})
}
}, [navigation])
}
Note: I'm passing the navigation prop down the hook since it's also used in the screen, but it could also be obtained from React Navigation's useNavigation hook.
Code inside PayMembershipScreen:
const PayMembershipScreen = ({navigation}) => {
useAssertInitialScreen({ navigation, name: "MembershipHomeScreen" })
return (
<View>
<Text>
This is the PayMembershipScreen AKA the target
</Text>
</View>
)
}
Demo
Here's a snack with the code of this answer:
https://snack.expo.dev/#ger88555/tabs-with-stacks-and-a-button---a-solution
I still want to see whether there's a cleaner solution than handling the "Pay" button's functionality inside its target screen.
I'll mark as the accepted answer any other solution that gets the expected outcome :)

How to get navigation object outside Screen

I have the following code in the App.tsx:
export default function App() {
...
return (
<NavigationContainer>
<Drawer.Navigator
initialRouteName="StackNavigation"
drawerContent={(props) => <MainPageDrawer {...props} />}
screenOptions={{ swipeEnabled: false }}
>
<Drawer.Screen name="StackNavigation" component={StackNavigator} />
</Drawer.Navigator>
</NavigationContainer>
);
...
}
As you see, the MainPageDrawer component isn't inside the Drawer.Screen component. But I still need to get access to navigation object inside MainPageDrawer. When I do something lake this inside MainPageDrawer:
const navigation = useNavigation<MainScreenNavigationProp>();
I get the following error:
Couldn't find a navigation object. Is your component inside a screen in a navigator?
What is the right way to get access to the navigation object outside Screen component?
Solution: I figured out that useNavigation won't work anyway if it is outside Screen, BUT this:
(props) => <MainPageDrawer {...props} />
still pass navigation through props, so you can easily access it without useNavigation. I simply restructured navigation:
function MainPageDrawer({ navigation }) {
...
}
And it works

React navigation createMaterialTopTabNavigator inside a screen

React navigation createMaterialTopTabNavigator shows a pretty cool setup at the top of their documentation page:
Notice that the top navigator stack seems to be inside a screen. There is a title at the top "Material Top Tabs" and we can go back to "Examples"
Funny enough, this setup is not given in any of the examples.
I would like to have the same setup and be able to navigate to a specific tab from another screen.
Could someone please share an example code for it?
What I currently have is a basic bottom tab navigation with a
<BottomTab.Screen
name="Tabs"
component={TabsNavigator}
/>
and then
function TabsNavigator() {
const MyTabs = createMaterialTopTabNavigator();
return (
<MyTabs.Navigator
<MyTabs.Screen name="Upcoming" component={TabsUpcomingScreen} />
<MyTabs.Screen name="Past" component={TabsPastScreen} />
</MyTabs.Navigator>
);
}
Although this works when doing navigation.navigate('Tabs', { screen: 'Upcoming' });, I am missing the screen containing the tabs... or the "Tabs" screen (as in the example, it is the "Material Top Tabs" screen)
I could do something like this:
function TabsNavigator() {
const TabsStack = createStackNavigator();
return (
<TabsStack.Navigator>
<TabsStack.Screen name="Tabs" component={TabsScreen} options={{ headerTitle: 'Tabs' }} />
</TabsStack.Navigator>
);
}
function TabsScreen() {
const MyTabs = createMaterialTopTabNavigator();
return (
<MyTabs.Navigator
<MyTabs.Screen name="Upcoming" component={TabsUpcomingScreen} />
<MyTabs.Screen name="Past" component={TabsPastScreen} />
</MyTabs.Navigator>
);
}
But then I am unable to navigate to a specific tab with navigation.navigate('Tabs', { screen: 'Upcoming' });
If you want to navigate to some tab inside of createMaterialTopTabNavigator.Instead of navigation.navigate('Tabs', { screen: 'Upcoming' }) use navigation.jumpTo('Tabs', { name: 'Upcoming' });
https://reactnavigation.org/docs/material-top-tab-navigator#jumpto

How to load another screen on top of Home screen when we click on icon in header in Drawer navigation in React-Native

I have created a Drawer Navigation with Home and Cart menu
when i click on Cart symbol on header i want to open separate page on top of this home screen.
I have written code like below
const DrawerNavigationCon=createDrawerNavigator({
Home:HomeNavigation,
Cart:CartNavigation
},{
overlayColor:'gray',
initialRouteName:'Home'
})
and i have written code to navigate from home to cart is like below
const CartNavigation = createStackNavigator({
Cart:CartPage,
},
{
defaultNavigationOptions:({navigation})=>{
return{
headerStyle:{
backgroundColor:'rgb(28, 34, 36)'
},
headerTitle:'Cart',
headerTintColor:'#fff',
headerTitleStyle:{
fontWeight:'bold',
textAlign:'center',
flex:1
},
headerLeft:(
<View>
<Icons name="md-menu" style={{fontSize:35,color:'white',paddingLeft:10}} onPress={()=>navigation.openDrawer()} />
</View>
),
headerRight:(
<View>
<Icons name="md-cart" style={{fontSize:30,color:'white',paddingRight:10}} onPress={() => navigation.navigate('Cart')} />
</View>
)
}
}
})
you can see onPress={() => navigation.navigate('Cart')} in <Icons> Tag
when i click on Cart it's opening like below
But i want to load another page on top of Home page when i click cart like below
how to do like this, please help
Thanks in advance
const DrawerNavigationCon= createStackNavigator({
Home:createDrawerNavigator({
Home:HomeNavigation,
},{
overlayColor:'gray',
initialRouteName:'Home'
}),
Cart:{
screen : CartPage
}
})
Use your stack configuration as shown and configure your stack as per requirement
You can get an idea from the above code.
Basically idea is to use stackNavigator as parent stack and put drawer navigator as child of that stackNavigator and put another route as parallel of that drawer navigator so you can now able to open another screen at top of drawer navigator.

back button with react-navigation on specific screen dynamically

on react-navigation i am trying to navigate to a specific screen which depends on some condition!
Below it is pointing to 'App' page right now
but sometimes i need it to go to 'Main' page
static navigationOptions = ({ navigation }) => {
return {
title: 'Tools',
headerLeft: (
<Button
onPress={() => navigation.navigate('App')}
title="Back"
/>
),
};
};
when i use state or function
for example
...
...
onPress={() => navigation.navigate(this.someFunction())}
...
...
there is an error undefined is not an object
so how do I point dynamically the desirable page to navigate to?
if came from any screen , want to go back dynamic without defining the screen name in navigation . Use this method :
onPress={()=>navigation.goBack()}
navigation.goBack() method will take you directly on the screen where it came from .

Resources