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.
When the MembershipStack
tab has not yet been loaded (i.e. when the App just launched), the following happens:
HomeScreen
.PayMembershipScreen
.PayMembershipScreen
becomes the initial screen on MembershipStack
.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)?
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:
HomeScreen
to PayMembershipScreen
successfuly.MembershipHomeScreen
from PayMembershipScreen
.MembershipHomeScreen
does not render in-between during the transition.However:
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.
Is anyone able find a solution that achieves the expected outcome? To sum up:
HomeScreen
to PayMembershipScreen
.MembershipHomeScreen
once they're in PayMembershipScreen
.MembershipHomeScreen
should not flash in the transition from HomeScreen
to PayMembershipScreen
.useLayoutEffect
).MembershipHomeScreen
once they're inPayMembershipScreen
, pressing the "Pay" button on HomeScreen
again should open the PayMembershipScreen
again (no buggy behavior).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
I couldn't get your solution to work for me.
Instead, I used a version of your proposed remedy #2, except that inside of the intermediary screen (which would be MembershipHomeScreen
in your example, I used useEffect
, looked for the parameter that I had specified in the previous screen (HomeScreen
, in your example), and then called navigation.push()
, rather than navigation.navigate()
. Note that push()
is only available when the navigator is a StackNavigator
. The parameters are the same, though.
There is a quick flash of the intermediary screen, but no freeze. Although, it should be noted that in my case, the intermediary screen is very lightweight, with no effects other than logging a Google Analytics event asynchronously.
HomeScreen:
const payPressHandler = () => {
navigation.navigate("MembershipStack",
{
screen: "MembershipHomeScreen",
params: {
navToNested: {
name: 'PayMembershipScreen',
params: {
customParamForDestinationScreen: 'foo'
}
}
}
}
);
}
MembershipHomeScreen:
useEffect(() => {
if (route?.params?.navToNested) {
const navTo = route.params.navToNested;
navigation.push(navTo.name, navTo.params);
}
}, [route]);
This still doesn't feel like the cleanest, most ideal solution, but it works, more or less. It is what I am using in my own app for now. Hopefully, it is of some help to the original poster, or to others who read this.