useIonViewWillLeave and useIonViewDidLeave not triggering

Hi there.

I’m trying to do some cleanup events when leaving ionic views but neither method is firing on any page of my app. I thought it may have been due to how the routing is declared, but I can’t find any solution.

        <IonApp>
            <IonReactRouter>
                <IonRouterOutlet> 
                    <Route path="/login" render={() => user ? <Redirect to="/home/pending-chats"/> : <Login/>} exact={true}/>
                    <Route path="/pending-chats/:contactId" component={LinkChat} />
                    <Route path="/conversations/:contactUserSwitchboardId" component={Chat} />
                    <Route path="/contacts/:contactId" component={ContactDetails} />
                </IonRouterOutlet>
                <Route path="/home" >
                    <IonTabs>
                        <IonRouterOutlet>
                            <Route path="/home/pending-chats" component={PendingChats} exact={true} />
                            <Route path="/home/conversations" component={Conversations} exact={true} />
                            <Route path="/home/contacts" component={Contacts} exact={true} />
                            <Route path="/home/configuration" component={Configuration} exact={true} />
                        </IonRouterOutlet>
                        <IonTabBar slot="bottom" style={{borderTop: 0}}>
                            <IonTabButton tab="pending-chats" href="/home/pending-chats">
                                {app.unreadPendingChats > 0 &&
                                    <IonBadge className="notification-tabs" color="primary">
                                        {app.unreadPendingChats < 100 ? app.unreadPendingChats : '99+'}
                                    </IonBadge>
                                }
                                <IonIcon icon={peopleCircle} />
                                <IonLabel>{t('pendingChats.navTitle')}</IonLabel>
                                <IonRippleEffect />
                            </IonTabButton>
                            <IonTabButton tab="conversations" href="/home/conversations">
                                {app.unreadConversations > 0 &&
                                    <IonBadge className="notification-tabs" color="primary">
                                        {app.unreadConversations < 100 ? app.unreadConversations : '99+'}
                                    </IonBadge>
                                }
                                <IonIcon icon={chatbubblesOutline} />
                                <IonLabel>{t('conversations.navTitle')}</IonLabel>
                                <IonRippleEffect />
                            </IonTabButton>
                            <IonTabButton tab="contacts" href="/home/contacts">
                                <IonIcon icon={personCircleOutline} />
                                <IonLabel>{t('contacts.navTitle')}</IonLabel>
                                <IonRippleEffect />
                            </IonTabButton>
                            <IonTabButton tab="configuration" href="/home/configuration">
                                <IonIcon icon={options} />
                                <IonLabel>{t('configuration.navTitle')}</IonLabel>
                                <IonRippleEffect />
                            </IonTabButton>
                        </IonTabBar>
                    </IonTabs>
                </Route>
                <Route render={() => user ? <Redirect to="/home/pending-chats" /> : <Redirect to="/login" />} />
            </IonReactRouter>
        </IonApp>

You didn’t show the content of any of your components, but I’m pretty sure that to get the transitions to work, the direct child of each <Route> needs to be an <IonPage>, so make sure that <LinkChat>, <Chat>, <ContactDetails> and so on are wrapped in <IonPage>.

Sure, just as an example, this is the login page:

const Login = () => {

    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [loading, setLoading] = useState(false);
    const [errors, setErrors] = useState({
        login: false,
        server: false
    });

    useIonViewDidEnter(() => {
        unregister();
    }, []);

    const {update} = useContext(AppContext);

    const router = useIonRouter();

    const loginSubmit = (e) => {

        e.preventDefault();

        setLoading(true);

        setErrors({
            login: false,
            server: false
        });

        AuthService.login(email, password).then(async () => {
                checkNotifications();
                const user = await AuthService.getLoggedUser();
                update({user});
                router.push("/home/pending-chats");
            },
            (error) => {
                if (error?.response?.data?.errorCode === 12011) {
                    setErrors({...errors, login: true});
                } else {
                    setErrors({...errors, server: true});
                }
            }
        ).finally(() => {
            setLoading(false);
        });
    }

    useIonViewWillEnter(async () => {
        if (await AuthService.getCurrentUser()) {
            router.push("/home/pending-chats");
        }
    });

    useIonViewWillLeave(() => {
        console.log("Login will leave");
    }, []); // tried to use it without the dependency array too

    return (
        <IonPage>
            <IonLoading isOpen={loading} />
            <IonContent>
                <IonGrid style={{height: "100vh"}}>
                    <IonRow className="ion-align-items-center" style={{height: "100vh"}}>
                        <IonCol size="12">
                            <form onSubmit={loginSubmit} style={{marginTop: "auto", marginBottom: "auto"}}>
                                <IonRow className="ion-justify-content-center">
                                    <IonCol size="12" className="ion-margin-bottom ion-margin-top ion-text-center">
                                        <img src="/assets/img/logo.png" alt="logo"></img>
                                    </IonCol>
                                    <IonCol size="10" className="ion-margin-top">
                                        <IonItem>
                                            <IonLabel position="stacked" color="medium" className="ion-text-center">{t('login.email')}</IonLabel>
                                            <IonInput type="email" className="ion-text-center" value={email} onIonChange={(e) => setEmail(e.target.value)} required />
                                        </IonItem>
                                    </IonCol>
                                    <IonCol size="10">
                                        <IonItem>
                                            <IonLabel position="stacked" color="medium" className="ion-text-center">{t('login.password')}</IonLabel>
                                            <IonInput type="password" className="ion-text-center" value={password} onIonChange={(e) => setPassword(e.target.value)} required />
                                        </IonItem>
                                    </IonCol>
                                    <IonCol size="12" className="ion-text-center">
                                        <IonButton fill="outline" mode="ios" type="submit" className="ion-margin-top">
                                            {t('login.button')}
                                        </IonButton>
                                    </IonCol>
                                    <IonCol size="12">
                                        <Alert icon={alertCircleOutline} isOpen={errors.server} message={t("login.error.server")} color="white" textColor="danger" />
                                        <Alert icon={alertCircleOutline} isOpen={errors.login} message={t("login.error.initNoLogin")} color="white" textColor="danger" />
                                    </IonCol>
                                </IonRow>
                            </form>
                        </IonCol>
                    </IonRow>
                </IonGrid>
            </IonContent>
            <IonFooter>
            </IonFooter>
        </IonPage>
    );
}

export default Login;

As I said, I tried using useIonViewDidLeave, rearranging the app routes, etc. and I do not know what to do anymore

I am not an expert on the routing system, but the problem might be your use of router.push(). The Ionic Docs (React Navigation: Router Link Redirect to Navigate to Another Page) recommend using history.push.

Still not getting to trigger the hook, so definitely not the reason it’s not working. Am I missing something or is this kind of behaviour unusual?

The hooks should be triggered if your routes are defined correctly-- I have an app with several hundred routes, including manually defined routes, dynamically defined routes, and nested routes, and I have not had trouble getting IonViewDidEnter/Leave to trigger.

Looking at your routes, one thing I would try is to disable your <Route path="/home"> route section and see if you can get the hooks to work.

The issue with the <Route path="/home"> section is that the home route is not wrapped in <IonRouterOutlet>, which is what is managing the transitions between IonPages.

So the structure should be something like:

       <IonRouterOutlet> 
                <Route path="/login" render={() => user ? <Redirect to="/home/pending-chats"/> : <Login/>} exact={true}/>
                <Route path="/pending-chats/:contactId" component={LinkChat} />
                <Route path="/conversations/:contactUserSwitchboardId" component={Chat} />
                <Route path="/contacts/:contactId" component={ContactDetails} />
                <Route path="/home" component={routerOutletHome} />
            </IonRouterOutlet>

And then component routerOutletHome:

                 <IonPage>
                    <IonRouterOutlet>
                        <Route path="/home/pending-chats" component={PendingChats} exact={true} />
                        <Route path="/home/conversations" component={Conversations} exact={true} />
                        <Route path="/home/contacts" component={Contacts} exact={true} />
                        <Route path="/home/configuration" component={Configuration} exact={true} />
                    </IonRouterOutlet>
                  </IonPage>

Note that the nested router outlet is wrapped in <IonPage>. This is necessary to prevent the IonRouterOutlets from fighting each other.

Anyway, nested routes have several tricky points, so first I would remove the route nesting from your app and test the hooks that way.

The problem of wrapping the outlet with <IonPage> is that it throws an error

Uncaught Error: IonTabs must contain an IonRouterOutlet

I suppose <IonTabs> expects a <IonRouterOutlet> as a direct child. And if I wrap the whole <IonTabs>, tabs are showing outside the tab pages and the page stops working

What I’ve noticed is that all the solutions (that I’ve seen) that allows me to have routes without tabs, disable those hooks because it’s probably confused about the routing. Would love if there was some “official way” of having both tab and tabless routes

You can have both tabbed and tabless routes.

To do so, one way is to put the routes that should have tabs in their own component with <IonTabs>, the router for those tabs, and everything else as described here.

Just forked the repo you sent to reproduce my issue and this is what I can show:

  • When leaving a tabless component, neither hook will trigger, regardless if you enter a tab or tabless component
  • When leaving a tab component, the hook only triggers if you enter another tab components (or if you “switch tabs”)

At this point I suppose this is a bug, unless there is something I’m not getting right

The tabless components aren’t in <IonRouterOutlet>.

See this section of the docs. The routes must be wrapped in <IonRouterOutlet> for the lifecycle methods to be available.

1 Like

This almost doest it, the only problem remaining is when switching from a tab view into a tabless one. Also I thought it was bad to wrap nested <IonRouterOutlet> components, but as long as it does the trick I’m on it (since if you don’t, it seems the problem persists between tab and tabless, and you can’t remove the tab <IonRouterOutlet>)

Still, this is quite useful to know, it just never ocurred to me to nest <IonRouterOutlet>

As I wrote earlier in the thread, you have to wrap the nested <IonRouterOutlet> in <IonPage>. You need to do this because <IonPage> inside <IonRouterOutlet> tells Ionic it needs to do a transition and fire the lifecycle events. If you do not do this, the events do not get fired.

So, in short, to get the lifecycle events to fire, you need two components: <IonRouterOutlet> and <IonPage>. Inside <IonRouterOutlet>, each <IonPage> will fire the lifecycle events.

But! If you have a nested <IonRouterOutlet>, it needs to be wrapped in an <IonPage> the same as any other component to get the lifecycle methods to fire.