Pushing a screen from outside of bottom tab navigation opens the screen in previous active bottom tab.
Current behavior
My root stack
<Stack.Navigator>
<Stack.Screen
name="BottomTabNavigation"
component={BottomTabNavigation}
/>
<Stack.Screen
name={'ScreenWithoutTab'}
key={'ScreenWithoutTab'}
component={ScreenWithoutTab}
/>
</Stack.Navigator>
My BottomTab
<Tab.Navigator
initialRouteName={'Home'}>
<Tab.Screen
name="Home"
key={'Home'}
component={HomeStack}
/>
<Tab.Screen
name="Settings"
key={'Settings'}
component={SettingsStack}
/>
</Tab.Navigator>
HomeStack
<Stack.Navigator
initialRouteName={'HomeScreen'}>
<Stack.Screen name="HomeScreen" key={'HomeScreen'} component={Home} />
<Stack.Screen name="TestScreen" key={'TestScreen'} component={Test} />
</Stack.Navigator>
SettingsStack
<Stack.Navigator
initialRouteName={'SettingsScreen'}>
<Stack.Screen name="SettingsScreen" key={'SettingsScreen'} component={Settings} />
<Stack.Screen name="TestScreen" key={'TestScreen'} component={Test} />
</Stack.Navigator>
Problematic Scenario
- I open the app and the bottom tab navigation appears with initial tab HOME
- I switch to Settings Tab
- I switch to Home Tab
- I press on a button on Home Screen to open the screen ScreenWithoutTab
- In ScreenWithoutTab screen, i press a button which calls following functions
navigation.push('TestScreen')
navigation.goBack()
- The screen opens at Settings Tab. But my latest active tab was Home as you can see at Step 3.
If i dont do the Step 2., everything works as expected the TestScreen opens at Home tab
Expected behavior
If I open a screen from the stack which is above bottom tab navigation and when a screen (which is nested in bottom tab) is pushed from that screen, the current tab should try to open the screen not previous selected tab.
Reproduction
Steps to reproduce described above.
Platform
- [X] Android
- [X] iOS
- [ ] Web
- [ ] Windows
- [ ] MacOS
Packages
- [X] @react-navigation/bottom-tabs
- [ ] @react-navigation/drawer
- [ ] @react-navigation/material-bottom-tabs
- [ ] @react-navigation/material-top-tabs
- [X] @react-navigation/stack
- [ ] @react-navigation/native-stack
Environment
- [] I've removed the packages that I don't use
| package | version |
|---|---|
| @react-navigation/native | 6.0.8 |
| @react-navigation/stack | 6.1.1 |
Hey! Thanks for opening the issue. The issue doesn't seem to contain a link to a repro (a snack.expo.dev link, a www.typescriptlang.org/play link or link to a GitHub repo under your username).
Can you provide a minimal repro which demonstrates the issue? A repro will help us debug the issue faster. Please try to keep the repro as small as possible and make sure that we can run it without additional setup.
Couldn't find version numbers for the following packages in the issue:
@react-navigation/bottom-tabs@react-navigation/drawer@react-navigation/material-bottom-tabs@react-navigation/material-top-tabs
Can you update the issue to include version numbers for those packages? The version numbers must match the format 1.2.3.
The versions mentioned in the issue for the following packages differ from the latest versions on npm:
@react-navigation/native(found:6.0.8, latest:6.0.10)@react-navigation/stack(found:6.1.1, latest:6.2.1)
Can you verify that the issue still exists after upgrading to the latest versions of these packages?
The issue persists after package updates
Just run into this issue too. I'm following the recommended way of hiding the tab bar on certain screens (https://reactnavigation.org/docs/hiding-tabbar-in-screens/) and this kind of kills it for us currently.
When the 'outside tabs' navigate action is fired it seems like it doesn't prioritise the active tab as being the 'closest' navigator inside the tabs, rather one of the others is considered closer (the last one lazy-loaded maybe)?
Minimal repro: https://snack.expo.dev/@liam/react-navigation-issue-10527-minimal-repro
What I did
- Switch tabs from Home to MyHealth to Library
- Go back to MyHealth tab
- Follow links to Questionnaires -> TakeQuestionnaire -> QuestionnaireCompleted
What I expected
QuestionnaireCompleted opened in the last active tab (MyHealth)
What I saw
QuestionnaireCompleted opened in an inactive tab (Library)
Okay, I've found out why this happens and we're trying to see if we can fix it/workaround it since it's blocking our current work.
The behaviour of "fire the navigate action against the last initialised tab" happens because useOnAction() is searching through the child navigators' action listeners to find the first one that can respond to the action. When it does this, it loops over the actionListeners array (which comes from useChildListeners()) backwards, starting with the last item in the array: https://github.com/react-navigation/react-navigation/blob/5b13f818f3c701ce33c2a30f5dd96763069556e4/packages/core/src/useOnAction.tsx#L130 The last initialised tab is picked up first because it was the last navigator to call addListener() to add to this list.
Ideally here, we want to prioritise looking at the focused route before any others. However, we can't currently do that as we have an array of listeners and no reference to who they belong to.
There appears to be two different ways within the code that listeners are setup currently;
useChildListeners()- foractionandfocususeKeyedChildListeners()- forgetStateandbeforeRemove
The listeners registered via the latter method are done so against the navigator key so the navigator can be refound. There don't seem to be many references to useChildListeners internally (and it's not exposed on the public API). Our currently intended change is to delete useChildListeners, move action and focus listener code to use useKeyedChildListeners so that, inside useOnAction we can prioritise the focused navigator listeners via a check against state.routes[state.index].key before the others are looked at in the previous order (newest listener first).
Thank you for the detailed explanation about the cause of the issue. I hope, it will be fixed because we can't be the only two people who has that problem or we are doing something wrong
we can't be the only two people who has that problem
Indeed, but I suspect most people don't often run into it as it's related to having the same screen name in multiple navigators.
or we are doing something wrong
and if we are, I hope someone lets us know! 😄
@swizes Doris on our team has now raised a draft PR that could potentially fix this issue if you want to take a look: https://github.com/react-navigation/react-navigation/pull/10596 At first blush, it's working for us.
It's not complete yet - we've got a few lint errors and tests which will need updating but we raised it as-is so we can get some early feedback from the team as to if we're even approaching this in the correct manner.
Great work, I am going to look into it.
@swizes FYI if you haven't been following the PR, it's not going to be accepted to the project - https://github.com/react-navigation/react-navigation/pull/10596#issuecomment-117110115 - AND a related FYI for a React Navigation v7+ change in behaviour that may affect you:
...we recommend explicitly navigating to the nested screen you want: https://reactnavigation.org/docs/nesting-navigators/#navigating-to-a-screen-in-a-nested-navigator
In React Navigation 7, this will be disabled by default and require using the API to navigate to a screen in a nested navigator. It'll be eventually removed in another version.
kkkkkkk
Hi @liamjones and @swizes, I don't know if you managed to solve your issue but I think I have a solution that may work for you.
Basically, I have the same stack as you, and when I want to navigate to TestScreen I don't know if it's the TestScreen from HomeStack or SettingsStack. I don't know if it's a bug, so I ended up attaching a screenListener on a focus event of my TabNavigator, and wrapping all of that in a context. My solution look like this :
import {
createContext,
useContext,
useState,
useMemo,
PropsWithChildren,
} from 'react';
import { ParamListBase } from '@react-navigation/native';
type FocusedScreenData<T extends ParamListBase> = {
getCurrentScreen: () => keyof T;
setCurrentScreen: (screenName: keyof T) => void;
};
const getFocusedScreenContext = <T extends ParamListBase>(
defaultValue: FocusedScreenData<T>,
) => createContext<FocusedScreenData<T>>(defaultValue);
export const getFocusedScreenProvider = <T extends ParamListBase>(
initialScreen: keyof T,
) => {
const FocusedScreenContext = getFocusedScreenContext<T>({
getCurrentScreen: () => initialScreen,
setCurrentScreen: () => {},
});
const FocusedScreenProvider = ({ children }: PropsWithChildren<{}>) => {
const [focusedScreen, setFocusedScreen] = useState<keyof T>(initialScreen);
const contextValue = useMemo<FocusedScreenData<T>>(
() => ({
getCurrentScreen: () => focusedScreen,
setCurrentScreen: (screenName) => setFocusedScreen(screenName),
}),
[focusedScreen, setFocusedScreen],
);
return (
<FocusedScreenContext.Provider value={contextValue}>
{children}
</FocusedScreenContext.Provider>
);
};
const useFocusedScreen = () => useContext(FocusedScreenContext);
return { FocusedScreenProvider, useFocusedScreen };
};
My TabNavigator
export const {
FocusedScreenProvider: FocusedTabProvider,
useFocusedScreen: useFocusedTab,
} = getFocusedScreenProvider<MainTabParamList>("HomeStack");
const HomeScreen = () => {
const { setCurrentScreen } = useFocusedTab();
return (
<HomeTabNavigator.Navigator
screenListeners={({ navigation }) => ({
focus: ({ target }) => {
if (target) {
const { routes } = navigation.getState();
const focusedRoute = routes.filter((route) => route.key === target);
setCurrentScreen(focusedRoute[0].name);
}
},
})}
>
<HomeTabNavigator.Screen
name="HomeStack"
component={HomeStack}
/>
<HomeTabNavigator.Screen
name="SettingStack"
component={SettingStack}
/>
/>
);
}
// Root Navigator
<FocusedTabProvider>
<Stack.Navigator>
<Stack.Screen
name="BottomTabNavigation"
component={BottomTabNavigation}
/>
<Stack.Screen
name={'ScreenWithoutTab'}
key={'ScreenWithoutTab'}
component={ScreenWithoutTab}
/>
</Stack.Navigator>
</FocusedTabProvider>
And from the ScreenWithoutTab component :
const goToTest = useCallback(() => {
const currentTab = getFocusedScreen();
navigation.navigate(BottomTabNavigation, {
screen: currentTab,
params: {
screen: 'TestScreen',
params: {
// What ever params for test screen...
},
},
});
}, [navigation, getFocusedScreen]);
Hope it helps !
Thanks @ChrisYohann, sounds similar to what we did. The way we worked around it was by having a 'launcher' screen inside the shared stack and this takes care of the navigation when we return.
So rather than going StartViewInStack -> Modal and then having the Modal go 'navigate to ViewAInStack' or 'navigate to ViewBInStack' we go StartViewInStack -> Launcher -> Modal all in one operation (tweaked animation settings, etc to minimise the visibility) and then the Modal issues a goBack and the launcher (which has a focus listener) decides (based on redux state) whether to go to ViewAInStack or ViewBInStack.
It'd be nice if you could 'goBack' with parameters so redux wasn't involved but it's working reasonably well even without that.