Navigation back within react navigation from one stack to another stack - react-navigation

There are 2 ways to get to a Player screen.
You click on the 'More' tab which has a link for 'Teams' which displays all the teams available. Then you click on a Team and it brings you to the TeamStack.
In the TeamStack the default is the Team Home and then bunch of other screens, one is Roster, which lists all the players of the team. From there you get to the PlayerStack which brings you to the player home screen.
Now, when you get to the player screen you can swipe to go back, which will bring you back from where you came... to the Roster, then the team Home, then to the More page.
So far so good!
You click on the 'Stats' tab which links to the StatsStack. The default StatsStack screen lists the players leading the leaderboard. From there you can click on a player. From there you can click on the player's team, which brings you to the TeamStack. You can then click on the 'roster' again. If you go back from there everything works properly. Back to team home, back to player, back to stats. Still so far, so good!
Here is the problem...
Let's go back to the Stats tab, then click a player, then go to a team, then go to the roster. Now, if you click on a new player, then go back, it goes back directly to the Stats screen, instead of going back to the Roster, then to the Team Home, then to the original Player, then to the Stats.
Somehow, because the PlayerStack is already open, when you go to a Player from within the Team Roster in the Route 2 option it brings you back to the originally open PlayerStack, instead of adding a new screen to the stack.
Does this make sense? How do I make is so that the when you follow Route 2 to get to a new player, when you go back it brings you back in the proper sequence of screens?
const MainStackNavigator = createStackNavigator(
{
MainTabNavigator: MainTabNavigator,
Team: {
screen: TeamStack,
navigationOptions: ({ navigation }) => {
return {
header: null,
}
}
},
Player: {
screen: PlayerStack,
navigationOptions: ({ navigation }) => {
return {
header: null,
}
}
},
)
const MainTabNavigator = createBottomTabNavigator(
...
Stats: {
screen: StatsStack,
navigationOptions: {
tabBarIcon: ({ focused }) => {
return <Icon
name="ios-stats"
iconStyle={{}}
color={focused ? '#000' : '#ccc'}
size={28} />;
},
},
},
More: {
screen: MoreStack,
navigationOptions: {
tabBarIcon: ({ focused }) => {
return <Icon
name="ios-more"
iconStyle={{}}
color={focused ? '#000' : '#ccc'}
size={28} />;
},
},
},
)

I could delete this question because I just figured it out after writing all that out, but the answer is that in the Roster page, instead of getting to the Player screen with Navigation.navigate('Player') I had to use Navigation.push('Player').
According to the docs, if the screen is already open it won't add another duplicate screen to the stack, it will just go back to the open screen. In order to force the screen onto to the stack you have to use the push method.
This will work in either of my two cases above.

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 Unmount a screen when moving to another in React Native

I'm developing a React Native app using React Navigation v4, React Hooks and ES6.
I have 2 bottom tabs (Movies, Shows) and 4 screens with the following Stack structure:
**Movies**
-- MovieList
-- MovieDetail
**Shows**
-- ShowList
-- ShowDetail
My scenario
1) Moving from Movie list to an individual movie page
MovieList contains a list of movies, when I click on one of them, I first fetch some API data then move to the MovieDetail screen like this
dispatch(apiFetchActions.fetchMovies(movieId)).then((response) => {
props.navigation.navigate({
routeName: "MovieDetail",
params: {
assetId: movieId,
assetName: movieTitle,
},
});
MovieDetail is now on top of the Movies stack and MovieList at the bottom
2) Moving to a different tab (navigation stack)
I then click on Shows (2nd Tab) which takes me to the ShowList using props.navigation.navigate('ShowList')
3) The problem
If I click on the Movies Tab, I expect to be moved back to MovieList but since MovieDetail was never unmounted, it is still at the top of the Movies stack meaning that I see an old screen. I have to click twice to go back to the MovieList screen.
I've read quite a few suggestions on how to use onFocus/onBlur subscription however I could not found a solution using React Hooks.
My ideal solution would be to find a way to listen to the onBlur status in MovieDetail screen possibly using useEffect hook and somehow unmount it before leaving.
I found a way to make it easier to always move to the initial top of the stack when you click on any bottom tab icons.
You simply need to add the on Press and the screen reference like this
Stars: {
screen: StarsNavigator,
navigationOptions: ({ navigation }) => ({
tabBarIcon: (tabInfo) => {
return (
<Ionicons name="ios-people" size={22} color={tabInfo.tintColor} />
);
},
tabBarLabel: Platform.OS === "android" ? <Text>Stars</Text> : "Stars",
tabBarOnPress: () => {
navigation.navigate("StarsList");
},
}),
},
Star is one of my screens in BottomTabNavigator and using navigation.navigate("You Screen") does the trick. So regardless in which level of the stack you find yourself, every time you click on the Star tab you always end up to the original top level.

Nativescript - Android TabView back button custom navigation

In an Nativescript application I need to implement a custom navigation scenario for Android when user click on material/soft back button.
For simplicity, starting with login-tabs template (https://github.com/ADjenkov/login-tabs-ng) and I want implement a navigation like Instagram, Facebook, WhatsApp, Pinterest, and many more ...
That's with the example of login-tabs template, when I navigate from the Players tab to the Teams tab and then I tap the back button I want to return to the Players tab (on the page I left in this tab).
Today as the navigation history of the Teams tab outlet is empty and the navigation history of the root outlet is empty, the application is closes. I wish it was close if I tap on the back button after returning to the Players tab and if navigation history of Players tab is empty.
I hope it's clear, tell me if it's not the case.
Is there a way to implement this behavior?
Finally I implemented a solution that's inspired by the response of #Manoj.
I listen to the activityBackPressed event and set args.cancel = true for prevent default behavior.
At each Tab change I save the Tab previously visited. Then at every activityBackPressed event I check if the current outlet can go back or not with this.routerExtension.canGoBack({ outlets: [this.tabVisibleName], relativeTo: this.activeRoute }).
If not I return to the previous tab programmatically if the list of tabs visited is not empty. If the list of tabs visited is empty I set args.cancel = false for exit the app.
If this.routerExtension.canGoBack({ outlets: [this.tabVisibleName], relativeTo: this.activeRoute }) return true I simply go back : this.routerExtension.back({ outlets: [this.tabVisibleName], relativeTo: this.activeRoute });
Note : you must remove listener when application is going to background, otherwise you will have several listeners (one by resume) :
application.on(application.exitEvent, (args) => {
if (args.android) {
application.android.off(application.AndroidApplication.activityBackPressedEvent);
}
});
Thanks for your help
You need to save selected Tab in your data.service and when user go back to tabs.component.html you can use the selectedIndex. You can skip to listen to activityBackPressed as well in that case.
in your tabs.component.html
<TabView (selectedIndexChanged)="onSelectedIndexChanged($event)" [(ngModel)]="selectedTabIndex" width="100%">
and in your tabs.component.ts
constructor(
private routerExtension: RouterExtensions,
private activeRoute: ActivatedRoute,
private _dataService: DataService ) {
this.selectedTabIndex = this._dataService.selectedTabIndex;
}
and
onSelectedIndexChanged(args: SelectedIndexChangedEventData) {
const tabView = <TabView>args.object;
const selectedTabViewItem = tabView.items[args.newIndex];
this._dataService.selectedTabIndex = args.newIndex;
}

How to link the plus sign (add new record) to go to another web page

How do I link the plus sign at the bottom of the grid to go to a different web page as opposed to pop up a dialogue?
Currently there is a default dialogue popping up but I need it to link to a different web page.
Here is the answer
setupPager: function () {
$('#myGrid').jqGrid('navGrid', '#myPager',
{
addtitle: 'Add',
add: true,
addfunc: function () {
window.open(newpageurl);
},
//..more options go here
}
);
},

PhoneJS back button not working

I created a basic DevExpress PhoneJS v13.2.5 app using the slideout navigation type. I can switch between views nicely using the slideout menu (URL hash changes). However, the back button doesn't seem to work. I expect that when I hit the back button, I would be taken to the previous view based on the URL hash. However, I am instead taken to the previous page. So, for example, if my history is as follows:
www.google.com
localhost:4633
localhost:4633/#home
localhost:4633/#foo
localhost:4633/#bar
And I hit the back button (doesn't matter if I'm using Chrome, Android, etc), I would expect to be taken back to localhost:4633/#foo. However, I am taken back to www.google.com. Sometimes, before I am taken to the previous page, I briefly see the URL change to localhost:4633/#root.
Has this happened to anyone else? Am I missing something in my app configuration? From everything I've read in the documentation, it should "just work." Here is my app init:
"use strict";
var MyApp = window.MyApp = {};
$(function () {
MyApp.app = new DevExpress.framework.html.HtmlApplication({
namespace: MyApp,
navigationType: "slideout",
navigation: [
{
title: "Home",
action: "#home",
icon: "home"
},
{
title: "Foo",
action: "#foo",
icon: "info"
},
{
title: "Bar",
action: "#bar",
icon: "info"
}
]
});
MyApp.app.router.register(":view", { view: "home" });
MyApp.app.navigate();
});
The Back button is available in each view in a stack except for the root view. If you create a link with MyApp.app.navigate('Foo') in your Home view, click on that link and go to the "Foo" page, then you can back in the Home page by pressing back button. Please see:
Navigation History For further detailed informations.

Resources