Skip to content

bug: (react-router): lifecycle events do not fire on tab switches or routerDirection="none" navigation #31479

Description

@baenans

Prerequisites

Ionic Framework Version

v9.x

Current Behavior

In @ionic/react-router 9.0.x (checked on 9.0.5), switching tabs does not fire ionViewWillEnter, ionViewDidEnter, ionViewWillLeave or ionViewDidLeave. The same happens for any link with routerDirection="none", such as the menu items in the React side-menu starter. Pages that load their data in useIonViewWillEnter render, but the data never loads (as it'd do in i.e. 8.8.19).

The events are not dispatched at all. A capturing listener on the ion-router-outlet element receives nothing either, so this isn't a problem with the hooks.

Expected Behavior

Lifecycle events to fire on every navigation, whether or not it animates. The React lifecycle docs say ionViewWillEnter "is called every time the view is navigated to (regardless if initialized or not)".

Steps to Reproduce

  1. Clone the repro (official React tabs starter), then run npm install and npm run dev.
  2. Open /tab1. The page lists ionViewWillEnter and ionViewDidEnter, counter 2.
  3. Click Tab 2 in the tab bar. Observe that Tab 2 lists nothing, counter 0.
  4. Click Tab 1 again. Observe that Tab 1 still lists only its original two entries. Neither switch recorded a leave or an enter.
  5. Back on Tab 1, use the "Try these navigations" list. routerDirection="none" behaves like the tab bar: no events on either page. routerDirection="forward", routerDirection="root" and a forward push to the Details page (and the back button from it) all record the expected leave and enter entries.

Code Reproduction URL

https://github.com/baenans/ionic-9-router-issue-repro

Ionic Info

Ionic:

Ionic CLI : 7.2.1
Ionic Framework : @ionic/react 9.0.5

Capacitor:

Capacitor CLI : 8.5.2
@capacitor/core : 8.5.2

System:

NodeJS : v26.10.0
npm : 11.19.1
OS : macOS

Additional Information

It looks like the non-animated branch in StackManager.transitionPage() (L1644-L1646 in 9.0.5) skips routerOutlet.commit() and swaps the page classes itself, to avoid intermediate paints. Core dispatches the four lifecycle events from inside commit(), so skipping it drops them. In 8.8.19 this path called commit() with duration: 0 (L351-L357). Tab switches and routerDirection="none" both take this branch.

routerDirection="root" isn't a workaround. It unmounts the other pages in the outlet and drops back history, and it can't be used for tab switches.

This change has worked for us as a patch: dispatching the four events in the fast path in the same order commit() uses, around the class swap at StackManager.tsx L1735-L1746.

           await waitForComponentsReady();

           // Bail out if the component unmounted during waitForComponentsReady
           if (!this._isMounted) return;

+          // Fire lifecycle events since we're skipping commit() which normally dispatches them.
+          // Must run after waitForComponentsReady: on first mount PageManager registers the page
+          // before attaching its listeners, so earlier events would be lost.
+          // Skip leave events (and hiding) if a newer transition already made leavingEl the entering view
+          const isLeaving = myGeneration === this.transitionGeneration || leavingEl !== this.transitionEnteringElement;
+          if (isLeaving) {
+            leavingEl.dispatchEvent(new CustomEvent('ionViewWillLeave', { bubbles: false, cancelable: false }));
+          }
+          enteringEl.dispatchEvent(new CustomEvent('ionViewWillEnter', { bubbles: false, cancelable: false }));
+
           // Swap visibility synchronously - show entering, hide leaving
-          // Skip hiding if a newer transition already made leavingEl the entering view
           enteringEl.classList.remove('ion-page-invisible');
-          if (myGeneration === this.transitionGeneration || leavingEl !== this.transitionEnteringElement) {
+          if (isLeaving) {
             leavingEl.classList.add('ion-page-hidden');
             leavingEl.setAttribute('aria-hidden', 'true');
           }
+
+          enteringEl.dispatchEvent(new CustomEvent('ionViewDidEnter', { bubbles: false, cancelable: false }));
+          if (isLeaving) {
+            leavingEl.dispatchEvent(new CustomEvent('ionViewDidLeave', { bubbles: false, cancelable: false }));
+          }

Activity

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

Metadata

Metadata

Assignees

Labels

type: buga confirmed bug report

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions