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

How to dismiss modal containing a StackNavigator #686

Closed
davideweaver opened this Issue Mar 14, 2017 · 32 comments

Comments

Projects
None yet
@davideweaver

davideweaver commented Mar 14, 2017

I have a few StackNavigators with the root having mode: "modal"...

const AddStack = StackNavigator({
  Add1: { screen: Add1Screen },
  Add2: { screen: Add2Screen },
  Add3: { screen: Add3Screen }
}, {
  mode: "screen"
});

const Root StackNavigator({
  MainStack: { screen: MainStack },
  AddStack: { screen: AddStack }
}, {
  mode: "modal"
})

The AddStack modal is a 3-step series of screens. When the user is on the third screen (AddScreen3) I would like them to click Save and have the modal go away (and be back on MainStack).

It looks like the right way to do it would be using NavigationActions.reset, but I can't figure it out.

Can anyone shed some light on the right way to do it?

@davideweaver

This comment has been minimized.

davideweaver commented Mar 14, 2017

After looking into this more, I can achieve the results I want by swiping down on Add3Screen. However I need to be able to do it with a button click. Swiping down dispatches the following...

action Object {type: "Navigation/BACK", key: "id-1489519010411-0"}

It looks like that key is being generated automatically. Is there a way to assign or lookup the key so I can use it programmatically?

@davideweaver

This comment has been minimized.

davideweaver commented Mar 14, 2017

I've figured out a solution until I find a better one. The solution is based on the previous comment about dispatching a "Navigation/BACK" action with the key of the modal StackNavigator screen. I use the routeName to find the key, then dispatch the action. In my case the routeName is AddStack.

navActions.js (partial)

export function dismissModal(modalRouteName) {
  return async (dispatch, getState) => {
    var modalKey = findRouteKey(getState(), modalRouteName);
    if (modalKey) {
      dispatch(NavigationActions.back({key: modalKey}));
    }
  }
}

This goes in my screen component...

dispatch(navActions.dismissModal("AddStack")

see the full navActions.js at https://gist.github.com/davideweaver/34d701c402c199676916d471916f9dca

Maybe we can add a helper like this to NavigationActions in the future.

@blaues0cke

This comment has been minimized.

blaues0cke commented Mar 20, 2017

Thank you, @davideweaver. Your code helped me a lot to understand my very similiar problem. Actually my problem was that the modal was closed whenever I hit the "back" button inside the child StackNavigator.

I digged around a lot and found a solution by modifying the reducer. Since I am very new to redux and react-native, it would be great to get some feedback if this approach may be the "best-practice". Let me explain:

My top navigator is a StackNavigator. It contains at index 0 a instance of TabNavigator that represents my app. The following indexes are another StackNavigators that just offer some modal stuff like controllers for login or a camera view:

const MainTabNavigator = TabNavigator(
    {
        Tab1Home: { screen: Tab1WithNavigationState },
        // And so on
    }
);

const LoginRegisterStackNavigator = StackNavigator({
    Modal_Login: { screen: LoginScreen }
});

const ModalStackNavigator = StackNavigator({
    MainTabNavigator:                   { screen: MainTabNavigator                 },
    ModalSub_LoginScreenStackNavigator: { screen: LoginRegisterWithNavigationState }
}, {
    headerMode: 'none',
    mode:       'modal'
});

Since the Navigation/NAVIGATE event is fired to all reducers, I decided to start the name of all modal routes with ModalSub_ to detect them later.

With this approach, I can count all opened and closed sub views of LoginRegisterStackNavigator in ModalNavigationReducer, too. So I added a counter that just returns the old state (with an updated counter) until the counter reached zero. Then I return the new state that closes the modal, too:

const ModalNavigationReducer = (state, action) => {
    if (action && state) {
        if (action.type == 'Navigation/NAVIGATE') {
            if (action.routeName.indexOf('ModalSub_') > -1) {
                state.modalSubViewCount = 0;
            } else {
                state.modalSubViewCount++;
            }
        } else if (action.type == 'Navigation/BACK') {
            if (state.modalSubViewCount > 0) {
                state.modalSubViewCount--;
                return state;
            }
        }
    }

    // Default state handling here, results in newState

    return newState || state;
};

module.exports = ModalNavigationReducer;

To close the modal and reset its child stack navigator, I just fire a custom action:

this.props.navigation.dispatch({ type: 'Navigation/CLOSE_MODAL' });

And then I can update my reducers to reset and/or close the modal even when the stack is not empty:

const ModalNavigationReducer = (state, action) => {
    if (action && state) {
        if (action.type == 'Navigation/NAVIGATE') {
            if (action.routeName.indexOf('ModalSub_') > -1) {
                state.modalSubViewCount = 0;
            } else {
                state.modalSubViewCount++;
            }
        } else if (action.type == 'Navigation/BACK') {
            if (state.modalSubViewCount > 0) {
                state.modalSubViewCount--;
                return state;
            }
        } else if (action.type == 'Navigation/CLOSE_MODAL') {
            action.type = 'Navigation/BACK';
        }
    }

    // Default state handling here, results in newState

    return newState || state;
};

module.exports = ModalNavigationReducer;
const LoginRegisterNavigationReducer = (state, action) => {
    if (action && action.type == 'Navigation/CLOSE_MODAL') {
        return {
            index: 0,
            routes: [
                {
                    key: 'Init',
                    routeName: 'Modal_Login'
                }
            ]
        };
    }

    // Default state handling here, results in newState

    return newState || state;
};

module.exports = LoginRegisterNavigationReducer;

So I now just have to send the action Navigation/CLOSE_MODAL without caring about what modal view is opened right now.

@richardfickling

This comment has been minimized.

richardfickling commented Apr 6, 2017

My solution to this problem was to create a DismissableStackNavigator:

import React, { Component } from 'react';
import { StackNavigator } from 'react-navigation';
export default function DismissableStackNavigator(routes, options) {
  const StackNav = StackNavigator(routes, options);

  return class DismissableStackNav extends Component {
    static router = StackNav.router;

    render() {
      const { state, goBack } = this.props.navigation;
      const nav = {
        ...this.props.navigation,
        dismiss: () => goBack(state.key),
      };
      return (
        <StackNav
          navigation={nav}
        />
      );
    }
  }
};

For example:

const AddStack = DismissableStackNavigator({
  Add1: { screen: Add1Screen },
  Add2: { screen: Add2Screen },
  Add3: { screen: Add3Screen },
});

const Root StackNavigator({
  MainStack: { screen: MainStack },
  AddStack: { screen: AddStack }
}, {
  mode: "modal"
})

Now, in Add3Screen, you can call this.props.navigation.dismiss()

@justinmoon

This comment has been minimized.

justinmoon commented Apr 9, 2017

@richardfickling: Bravo. Works like a charm!

@stevenleeg

This comment has been minimized.

stevenleeg commented Apr 26, 2017

@richardfickling you are my hero! 😍 you have no idea how much time I've spent on trying to figure this out and your solution appears to have finally solved my issues.

@benoitvallon

This comment has been minimized.

benoitvallon commented Jun 9, 2017

Thank you, it works great. I think this should be in the official documentation in the advanced section as nested stackNav is quite common.

@almirfilho

This comment has been minimized.

almirfilho commented Jun 20, 2017

For those who still are in pain with this, the following solution works just fine:

this.props.navigation.dispatch(NavigationActions.back());

Found this solution here.

@yukkeechang

This comment has been minimized.

yukkeechang commented Jul 21, 2017

hey @richardfickling, I used your dismissNav which works like a charm btw but now I have a different issue. If a nest a bunch a screens using your dismiss nav inside another Stack nav, the first screen of my dismiss nav is also rendered in the stack nav meaning that if I want to go back or exit the stack, the first screen is actually rendered twice. How can I prevent that from happening?

@wkoutre

This comment has been minimized.

wkoutre commented Jul 30, 2017

Seems to me that:

this.props.navigation.dispatch(NavigationActions.back()))

is essentially the same as finding the key using something like what's below, then dispatching type: Navigation/BACK with the new key.

const getNextBackKey = keyStr => {
  const newKey = keyStr.split("");
  const lastNum = +newKey[newKey.length - 1];

  newKey[newKey.length - 1] = (lastNum + 1).toString();

  return newKey.join("").replace("Init-", "");
};
@spencercarli

This comment has been minimized.

Member

spencercarli commented Aug 28, 2017

Hi! In an effort to get the hundreds of issues on this repo under control I'm closing issues tagged as questions. Please don't take this personally - it's simply something we need to do to get the issues to a manageable point. If you still have a question I encourage you to re-read the docs, ask on StackOverflow, or ask on the #react-navigation Reactiflux channel. Thank you!

@lukewlms

This comment has been minimized.

lukewlms commented Oct 2, 2017

@richardfickling Thanks for the great solution. .dismiss() is available in this.props.navigation, but not in static navigationOptions = { navigation } => ...

Do you know why?

Solved this by setting params like this, but would be cleaner if both instances of navigation were the same.

  public componentDidMount() {
    this.props.navigation.setParams({dismiss: this.props.navigation.dismiss});
  }

@lukewlms

This comment has been minimized.

lukewlms commented Oct 13, 2017

We were using DismissableStackNavigator but switched to simply passing the parent navigation options through to screenProps, and accessing it there.

@sbkl

This comment has been minimized.

sbkl commented Nov 7, 2017

Hi @richardfickling and all, Used your solution on react-native v0.48. Just upgraded to 0.50 and it doesn't work anymore.The dismiss function is not in navigation props anymore. Anyone encountered the same issue?

@raggiadolf

This comment has been minimized.

raggiadolf commented Nov 8, 2017

@sbkl, I'm having the same problem after upgrading from react-navigation v1.0.0-beta.15 to v1.0.0-beta.19. Doesn't seem to have anything to do with react-native version.

-edit-
I get the same problem in beta.16

@martnu

This comment has been minimized.

martnu commented Nov 8, 2017

@sbkl @raggiadolf We solved it by using screenProps instead in DismissableStackNavigator and calling it using this.props.screenProps.dismiss(); in our screens.

import React, { Component } from 'react';
import { StackNavigator } from 'react-navigation';
export default function DismissableStackNavigator(routes, options) {
  const StackNav = StackNavigator(routes, options);

  return class DismissableStackNav extends Component {
    static router = StackNav.router;

    render() {
      const { state, goBack } = this.props.navigation;
      const props = {
        ...this.props.screenProps,
        dismiss: () => goBack(state.key),
      };
      return (
        <StackNav
          screenProps={props}
          navigation={this.props.navigation}
        />
      );
    }
  }
};
@sbkl

This comment has been minimized.

sbkl commented Nov 8, 2017

@martnu in which component do you put this code?

edit

ok got it. modified the initial function from @richardfickling with your code. Also, call this.props.screenProps.dismiss() in the related screen and it works!

thanks!

@lprhodes

This comment has been minimized.

Contributor

lprhodes commented Nov 22, 2017

Thanks @martnu !

Shame we're still having top use work-arounds like this

@tylerstephens814

This comment has been minimized.

tylerstephens814 commented Nov 27, 2017

thank goodness for @richardfickling who has saved my life on multiple occasions

@vlgusakov-ap

This comment has been minimized.

vlgusakov-ap commented Dec 12, 2017

Since React Native allows to pass props only through screenProps here is an updated way to dismiss a StackNavigator (based on @richardfickling's response)

import React, { Component } from 'react';
import { StackNavigator } from 'react-navigation';

export default function DismissableStackNavigator(routes, options) {
  const StackNav = StackNavigator(routes, options);

  return class DismissableStackNav extends Component {
    static router = StackNav.router;

    render() {
      const { state, goBack } = this.props.navigation;

      const screenProps = {
        dismiss: () => goBack(state.key),
      }
      return (
        <StackNav
          navigation={this.props.navigation} screenProps={screenProps}
        />
      );
    }
  }
};

And then you call this.props.screenProps.dismiss() from your component(s)

@tslater

This comment has been minimized.

tslater commented Dec 30, 2017

@vlgusakov-ap I'm having some trouble trying to figure out how to get screenProps to work. I'm using a stack navigator nested inside a drawer navigator. Any idea on how I could work this? I confirmed that calling:

const { state, goBack } = this.props.navigation;
goBack(state.key)

works. The issue is that I can't seem to get the "dismiss" screenProps to be defined on my screens. Could the parent drawer navigator be messing me up?

@LordParsley

This comment has been minimized.

LordParsley commented Feb 20, 2018

Following this approach, you might want to intercept the Android hardware back button in your view too:

  constructor(props: Props) {
    super(props);
    this.backButtonListener = null;
  }

  componentWillMount() {
    if (Platform.OS === 'android') {
      this.backButtonListener = BackHandler.addEventListener('hardwareBackPress', this.dismiss);
    }
  }

  componentWillUnmount() {
    if (Platform.OS === 'android' && this.backButtonListener) {
      this.backButtonListener.remove();
    }
  }

  dismiss = () => {
    this.props.screenProps.dismiss();
    return true;
  }
@hammadzz

This comment has been minimized.

hammadzz commented Mar 4, 2018

Using a back action with null key used to work for me but seems it broke with updating from v1.0 to v1.2.

NavigationActions.back({
	key: null
})

Do I have to go with dismissible stack navigator? This feels really hacky.

@brentvatne

This comment has been minimized.

Member

brentvatne commented Mar 6, 2018

@hammadzz - goBack(null) should still work, but only if you're on the first screen in the nested stack. see #3669 on a potential solution baked into react-navigation

@hammadzz

This comment has been minimized.

hammadzz commented Mar 6, 2018

@brentvatne that would be great if it gets merged. I want to dismiss the nested stack from the last screen. This used to work. I implemented the dismissible stack navigator and will remove it and use .dismiss() once it gets merged.

@ScreamZ

This comment has been minimized.

Contributor

ScreamZ commented Mar 7, 2018

@hammadzz @brentvatne

I also encountered the same issue one day. I solved using in a « Screen » from the AddStack navigator in the first example of that thread :

this.props.navigation.popToTop() // Reset all modal of modal stacks. (this is available since 1.0.0 I think).
this.props.navigation.goBack(null) // Then close modal itself to display the main app screen nav.

That did the job.

Maybe you can consider it for #3669 ? Instead of querying parent key from state ?

Regards

@LuckyCrab

This comment has been minimized.

LuckyCrab commented Mar 8, 2018

Thanks, @ScreamZ ! I modified my reducer:

export default (state = initialNavState, action) => {
  let nextState;
  let nextStateDefault = RootNavigator.router.getStateForAction(action, state);
  switch (action.type) {
      case 'Navigation/BACK':
          nextState = nextStateDefault;
          const activeChildRoute = state.routes[state.index];
          if(activeChildRoute.routeName === 'Modal') {
              const popToTopState = RootNavigator.router.getStateForAction(NavigationActions.popToTop(), state);
              nextState = RootNavigator.router.getStateForAction(NavigationActions.back(), popToTopState);
          }
          break;

, where 'Modal' is parent for StackNavigator with modal screens (ModalNavigator):

const RootNavigator = StackNavigator({
    Entrance: {screen: EntranceNavigator},
    Modal: {screen: ModalNavigator}
}, {headerMode: 'none', mode: 'modal'});

and now inside modal screen componen, only one call: navigation.dispatch(NavigationActions.back());

@hammadzz

This comment has been minimized.

hammadzz commented Mar 8, 2018

@ScreamZ wouldn't that first pop to top then dismiss? I don't want it to pop to top as that would look buggy to the end user.

@ScreamZ

This comment has been minimized.

Contributor

ScreamZ commented Mar 8, 2018

@hammadzz I'm not sure if popToTop is generating an animation, but even if it do it, it's quick enough to not being noticed, I use it already some a similar case.

@anhhai680

This comment has been minimized.

anhhai680 commented May 31, 2018

Hi everybody, I'm using react-native versions 0.50.3 but when I'm navigating to modal screen, I always get an error as capture belows.

capture

I integrated Redux with React Navigation and here's my AppNavigator code.

export const OrderStack = DismissableStackNavigator( { Cart: { screen: Cart }, Checkout: { screen: Checkout }, FinishOrder: { screen: FinishOrder } }, { headerMode: 'none' } );

export const MainStack = StackNavigator( { Home: { screen: Home, navigationOptions: { header: null } }, About: { screen: About }, Product: { screen: Product, navigationOptions: { tabBarVisible: false } }, ViewCart: { screen: OrderStack, navigationOptions: { header: null, tabBarVisible: false } }, List: { screen: List } }, { mode: 'modal' } );

export const AppNavigator = TabNavigator( { Main: { screen: MainStack, navigationOptions: { tabBarLabel: 'Trang chủ' } }, Products: { screen: List, navigationOptions: { tabBarLabel: 'Sản phẩm' } }, Cart: { screen: Cart, navigationOptions: { tabBarLabel: 'Giỏ hàng' } }, NotifyList: { screen: Notifications, navigationOptions: { tabBarLabel: 'Thông báo' } }, Contact: { screen: About, navigationOptions: { tabBarLabel: 'Liên hệ' } } }, { tabBarComponent: TabBarBottom, tabBarPosition: 'bottom', animationEnabled: false, swipeEnabled: false, lazy: true, initialRouteName: 'Main' } );

Here's AppWithNavigationState class code:

`class AppWithNavigationState extends React.Component {

static propTypes = {
    dispatch: PropTypes.func.isRequired,
    nav: PropTypes.object.isRequired
};

componentDidMount() {
    //initializeListeners("root", this.props.nav);
}

render() {
    const { dispatch, nav } = this.props;
    return (
        <AppNavigator
            navigation={addNavigationHelpers({
                dispatch,
                state: nav,
                addListener
            })} />
    );
}

}

const mapStateToProps = (state) => ({
nav: state.nav
});

export default connect(mapStateToProps)(AppWithNavigationState);`

Anybody can help me to resolve this issue. Thank you so much!

@ScreamZ

This comment has been minimized.

Contributor

ScreamZ commented May 31, 2018

@anhhai680 Are you using React navigation >= 2 ?

@brentvatne

This comment has been minimized.

Member

brentvatne commented May 31, 2018

hi there! if this is still a bug can you create a new issue that reproduces the bug on https://snack.expo.io? thanks!

@react-navigation react-navigation locked and limited conversation to collaborators May 31, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.