react-navigation icon indicating copy to clipboard operation
react-navigation copied to clipboard

Pushing a screen from outside of bottom tab navigation opens the screen in previous active bottom tab.

Open swizes opened this issue 3 years ago • 12 comments

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

  1. I open the app and the bottom tab navigation appears with initial tab HOME
  2. I switch to Settings Tab
  3. I switch to Home Tab
  4. I press on a button on Home Screen to open the screen ScreenWithoutTab
  5. In ScreenWithoutTab screen, i press a button which calls following functions
navigation.push('TestScreen')
navigation.goBack()
  1. 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

swizes avatar Apr 18 '22 10:04 swizes

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.

github-actions[bot] avatar Apr 18 '22 10:04 github-actions[bot]

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?

github-actions[bot] avatar Apr 18 '22 10:04 github-actions[bot]

The issue persists after package updates

swizes avatar Apr 18 '22 10:04 swizes

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)?

liamjones avatar May 11 '22 10:05 liamjones

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)

liamjones avatar May 11 '22 11:05 liamjones

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;

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).

liamjones avatar May 16 '22 13:05 liamjones

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

swizes avatar May 16 '22 13:05 swizes

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! 😄

liamjones avatar May 16 '22 13:05 liamjones

@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.

liamjones avatar May 17 '22 14:05 liamjones

Great work, I am going to look into it.

swizes avatar May 17 '22 15:05 swizes

@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.

liamjones avatar Aug 08 '22 10:08 liamjones

kkkkkkk

deepakp7566 avatar Sep 21 '22 14:09 deepakp7566

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 !

ChrisYohann avatar Oct 19 '22 19:10 ChrisYohann

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.

liamjones avatar Oct 20 '22 09:10 liamjones