Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TabNavigator Bottom reset stack when pressing selected tab #933

Closed
RyanMitchellWilson opened this issue Apr 5, 2017 · 27 comments
Closed

Comments

@RyanMitchellWilson
Copy link

This is more of a question/feature request than an issue. Is it possible with the TabNavigator to press on the active tab and have it reset it's stack? Much like most iOS TabNavigators if you are a few screens deep on a tab and want to get back to the initial route you can usually just press the current tab again and it will reset your stack. Is this currently possible and if not could this be added to the TabNavigator?

As an example if you have an iOS device just open the app store, navigate to an app then press the active tab and you will see expected behavior.

@tmaly1980
Copy link

You read my mind, I've been working on that all day. Still not quite there. I'll post details. Essentially, you need to customize tabBarComponent and pass along a custom jumpToIndex to the props which will call the proper navigation.

@tmaly1980
Copy link

This is incomplete, it's what I have so far, passed as parameters to TabNavigator (2nd argument):

{
 tabBarComponent: (props) => {
  		const { navigation, navigationState } = props;
  		const _jumpToIndex = props.jumpToIndex; // Just in case 
		return (<TabView.TabBarBottom {...props} jumpToIndex={(index) => {
			tab = navigationState.routes[index];
			tabRoute = tab.routeName;
			firstRoute = tab.routes[0].routeName; // navState is Tabs object

			navigation.navigate(firstRoute); // Goes to route, but adds to stack (w/back arrow)
		}} />)
	}
}   

@tmaly1980
Copy link

tmaly1980 commented Apr 5, 2017

This looks like it works, I tried chaining the routes together, but kept getting an issue with it not finding the subroute in the context of the original (previous) tab. Putting them in sequence seems to work ( the subroute is in the right context), but I'm not sure about a race condition if they're asynchronous

  tabBarComponent: (props) => {
  		const { navigation, navigationState } = props;
  		const _jumpToIndex = props.jumpToIndex; // Just in case 
		return (<TabView.TabBarBottom {...props} jumpToIndex={(index) => {
			tab = navigationState.routes[index];
			tabRoute = tab.routeName;
			firstRoute = tab.routes[0].routeName; // navState is Tabs object

			const tabAction = NavigationActions.navigate({ routeName: tabRoute });
			const firstScreenAction = NavigationActions.reset({ index: 0,
				actions: [ NavigationActions.navigate({ routeName: firstRoute }) ]
			});
			navigation.dispatch(tabAction);
			navigation.dispatch(firstScreenAction);
		}} />)
	}, 

@RyanMitchellWilson
Copy link
Author

@tmaly1980 That works pretty well for me. One problem though is that it resets every time you press the tab when I'm more looking for it only to reset if you are currently on the tab and select the same tab again. But this gets me pretty close, I'll see if I can figure out a way to have this work in my specific case and I'll post what I come up with.

@tmaly1980
Copy link

tmaly1980 commented Apr 6, 2017

@RyanMitchellWilson , maybe add to the beginning something like:

if(navigationState.index === index) { return }

It's probably not it, but you get the idea. Just console.log() the objects you have available to figure out what it should be.

@RyanMitchellWilson
Copy link
Author

RyanMitchellWilson commented Apr 6, 2017

Here is what I ended up with:

tabBarComponent: props => {
  const {navigation, navigationState} = props
  const jumpToIndex = index => {
    const lastPosition = props.getLastPosition()
    const tab = navigationState.routes[index]
    const tabRoute = tab.routeName
    const firstTab = tab.routes[0].routeName

    navigation.dispatch(pushNavigation(tabRoute))
    lastPosition === index && navigation.dispatch(resetNavigation(firstTab))
  }
  return <TabView.TabBarBottom {...props} jumpToIndex={jumpToIndex}/>
},

My actions are a bit different here but essentially the same logic. I'm using the getLastPosition function off props for my logic around resetting. Thanks for the help @tmaly1980 greatly appreciated!

@RyanMitchellWilson
Copy link
Author

Notice today there is a slight race condition here. While the app is animating to a new tab, if you keep pressing that tab, it will push multiple instances of that tab screen onto the stack.

@RyanMitchellWilson
Copy link
Author

Found the fix, just need to use navigationState.index over props.getLastPosition() and add a check before the dispatch to see which tab is the current.

tabBarComponent: props => {
  const {navigation, navigationState} = props
  const jumpToIndex = index => {
    const lastPosition = navigationState.index
    const tab = navigationState.routes[index]
    const tabRoute = tab.routeName
    const firstTab = tab.routes[0].routeName

    lastPosition !== index && navigation.dispatch(pushNavigation(tabRoute))
    lastPosition === index && navigation.dispatch(resetNavigation(firstTab))
  }
  return <TabView.TabBarBottom {...props} jumpToIndex={jumpToIndex}/>
},

@ldipasquale
Copy link

Thanks @RyanMitchellWilson!

@grabbou
Copy link

grabbou commented Apr 24, 2017

Closing in favour of #314

@grabbou grabbou closed this as completed Apr 24, 2017
@andreecosta
Copy link

This isn't working for me, do I need to do anything more?
I get the error:

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in.

Check the render method of `tabBarComponent`.

@akaone
Copy link

akaone commented May 26, 2017

@itswaze try using

return <TabBarBottom {...props} jumpToIndex={jumpToIndex}/>

instead of

return <TabView.TabBarBottom {...props} jumpToIndex={jumpToIndex}/>

@phoenixzsun
Copy link

@tmaly1980 Please hava a look at this issue: #1755 , We need help.

@phoenixzsun
Copy link

@tmaly1980 Hi. When I switched to a new tab, What I expected is that the stack of the tab is reseted. Can you help me with that?

@valinaga
Copy link

@tmaly1980, @RyanMitchellWilson great solution you found! Really great work!
However, I'm stuck with an issue. The tab navigator is not my top navigator but a screen in a StackNavigator. Something like this:
`
const AppNavigator = StackNavigator({
main: { screen: MainScreenNavigator },
login: { screen: LoginScreen },
recover: { screen: RecoverScreen },
register: { screen: RegisterScreen },
photo: { screen: PhotoScreen },
intro: { screen: IntroScreen }
});

const MainScreenNavigator = TabNavigator({
home: { screen: HomeNavigator },
mywishlist: { screen: MyWishBoxNavigator },
friends: { screen: FriendsNavigator },
messages: { screen: MessagesNavigator },
more: { screen: MoreNavigator }
});
`
Unfortunately I was not able to make your above code work. The issue was that dispatching the reset action was throwing an error because the route name must be from the AppNavigator and not from MainScreenNavigator.
Any hints will be highly appreciated.
Thanks in advance!

@ou2s
Copy link

ou2s commented Dec 11, 2017

@valinaga I found a dirty workaround for your usecase:

 tabBarComponent: ({ jumpToIndex, ...props }) => (
      <TabBarKeyboard
        {...props}
        jumpToIndex={async index => {
          const numberOfScreenInStack =
            props.navigationState.routes[index].routes.length;

          if (
            props.navigation.state.index === index &&
            numberOfScreenInStack > 1
          ) {
            for (let i = numberOfScreenInStack; i > 1; i--) {
              props.navigation.dispatch(NavigationActions.back());
            }
          } else {
            jumpToIndex(index);
          }
        }}
      />

If you found a cleaner solution let me know :)

@valinaga
Copy link

@ou2s hi, yes I finally found something. It works on 1.0.0-beta.21.

  navigationOptions: ({ navigation }) => ({
    tabBarOnPress: ({ previousScene, scene, jumpToIndex }) => {
      const { route, focused, index } = scene;
      if (!focused) {
        if (route.index > 0) {
          const tabRoute = route.routeName;
          const { routeName, key } = route.routes[0];
          navigation.dispatch(
            NavigationActions.navigate({ routeName: tabRoute })
          );
          navigation.dispatch(
            NavigationActions.reset({
              index: 0,
              key,
              actions: [
                NavigationActions.navigate({ routeName })
              ]
            })
          );
        } else {
          jumpToIndex(index);
        }
      }
    },
  })

@ou2s
Copy link

ou2s commented Dec 12, 2017

@valinaga thanks for sharing. However I was not able to run your code successfully...

I was able to make it work by doing this:

  tabBarOnPress: ({ previousScene, scene, jumpToIndex }) => {
    const { route, focused, index } = scene;
    if (focused) {
      if (route.index > 0) {
        const tabRoute = route.routeName;
        const { routeName, key } = route.routes[0];
        navigation.dispatch(
          NavigationActions.navigate({ routeName: tabRoute })
        );
        navigation.dispatch(
          NavigationActions.reset({
            index: 0,
            key,
            actions: [NavigationActions.navigate({ routeName })]
          })
        );
      } else {
        jumpToIndex(index);
      }
    } else {
      jumpToIndex(index);
    }
  }

@valinaga
Copy link

@ou2s depends on what you were looking for. I was trying to avoid reseting the stack if you already on a specific tab. Your version was doing the opposite, if that was what you were looking for. But when you go to a different tab, the corresponding stack won't reset until you press again.
Nevertheless, glad it's working.

@neilhamilton
Copy link

neilhamilton commented Dec 16, 2017

@valinaga This helped me huge and works great. If only we could remove the animation that happens when the nav of the tab is reset, as though it would appear that it was automatically at the index. Are you aware of anything? Thank you very much!

@valinaga
Copy link

@neilhamilton only solution I see is to disable the animation. Dispatching a navigation action will bring animation with it.

@tuanmai
Copy link

tuanmai commented Jan 12, 2018

@valinaga thank for your sharing. I changed your code a little bit that won't need to reset the stack, just go back.

navigationOptions: ({ navigation }) => ({
  tabBarOnPress: ({ scene, jumpToIndex }) => {
    const { route, focused, index } = scene;
    if (focused) {
      if (route.index > 0) {
        // eslint-disable-next-line immutable/no-let
        let currentIndex = route.index;
        while (currentIndex > 0) {
          navigation.dispatch(NavigationActions.back({}));
          currentIndex -= 1;
        }
      }
    } else {
      jumpToIndex(index);
    }
  },
});

@neilhamilton
Copy link

neilhamilton commented Jan 12, 2018

@tuanmai I ended up also using back, but instead of looping through each index and going back, you can do one fluid back motion by specifying a key to go back from in the Back Action

You could modify your code to find the key that is after the initial route and pass that to the back action:

navigationOptions: ({ navigation }) => ({
  tabBarOnPress: ({ scene, jumpToIndex }) => {
    const { route, focused, index } = scene;
    if (focused) {
      if (route.index > 0) {
        const { routeName, key } = route.routes[1]
        navigation.dispatch(NavigationActions.back({ key }))
      }
    } else {
      jumpToIndex(index);
    }
  },
});

@greghuels
Copy link

greghuels commented Feb 17, 2018

Here's another example that resets the stack on each tab change, assuming you have stack navigators for each of your tab screens:

function resetStack(navigation, routes) {
  if (routes.length > 1) {
    const { routeName } = routes[0];
    navigation.dispatch(NavigationActions.reset({
      index: 0,
      actions: [NavigationActions.navigate({ routeName })],
    }));
  }
}

And then in your TabNavigator:

  navigationOptions: ({ navigation }) => ({
    tabBarOnPress: ({ previousScene, scene, jumpToIndex }) => {
      resetStack(navigation, previousScene.routes);
      jumpToIndex(scene.index);
    },
  }),

@noymer
Copy link

noymer commented Mar 16, 2018

It seems that in v1.5.6 this feature is implemented!

c8531d0

@daveteu
Copy link

daveteu commented Jun 15, 2018

I've been trying all the above solutions and was confused why they don't work.

If you came here because jumpToIndex is not a function, please note for react navigator v2, the above is depreciated.

You should use

navigationOptions: ({ navigation }) => ({
    tabBarOnPress: ({navigation}) => { //Note the change.

   if  (!navigation.isFocused) {
  
  //do soundthing when it's not a focused tab. e.g Play a tab sound.

  }
    console.log(navigation); 
  }
})

Documentation here -> https://reactnavigation.org/docs/en/bottom-tab-navigator.html

@brentvatne
Copy link
Member

yeah this is also just implemented by default in react-navigation@^2

@react-navigation react-navigation locked and limited conversation to collaborators Jun 15, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests