Skip to content

bug: IonTabs.outlet is undefined when the first ionTabsWillChange fires (standalone Angular) #31674

Open

Description

@tzdevelopteam

Ionic version

@ionic/angular 9.0.9, Angular 22.2 (standalone, zoneless)

Current behavior

In the standalone IonTabs, outlet is typed IonRouterOutlet, but it is a non-static @ViewChild('outlet') on an ion-router-outlet rendered under *ngIf="tabs.length === 0". When a tabs page loads, the initial tab's activation emits stackWillChange with tabSwitch: true (there is no leaving view), so ionTabsWillChange fires during IonTabs' first change detection, before that query resolves. A handler that reads outlet throws TypeError: Cannot read properties of undefined (reading 'nativeEl') on every load of the tabs page.

Expected behavior

outlet is set whenever ionTabsWillChange fires, as its non-optional type says.

Steps to reproduce

<ion-tabs #tabs (ionTabsWillChange)="willChange(tabs)">
  <ion-tab-bar slot="bottom">…</ion-tab-bar>
</ion-tabs>
willChange(tabs: IonTabs) {
  console.log(tabs.outlet.nativeEl.children);
}

Load /tabs/tab1 directly: the first ionTabsWillChange throws. Later tab switches work.

Activity

  1. duongynhi000005-oss commented on Oct 30, 2026

    @duongynhi000005-oss

    Hi team, I traced #31674 in packages/angular/src/standalone/navigation/tabs.ts and packages/angular/src/common/directives/navigation/tabs.ts.

    Root Cause

    In packages/angular/src/standalone/navigation/tabs.ts:60, @ViewChild('outlet', { read: IonRouterOutlet, static: false }) outlet: IonRouterOutlet; is resolved after child view initialization because <ion-router-outlet #outlet> is inside *ngIf="tabs.length === 0". On initial route activation, <ion-router-outlet> emits (stackWillChange) before Angular assigns the @ViewChild property, so tabs.outlet is undefined inside the first ionTabsWillChange callback.

    Proposed Fix

    Pass the template reference #outlet directly to (stackWillChange)="onStackWillChange($event, outlet)" in tabs.ts and assign if (outlet && !this.outlet) { this.outlet = outlet; } in IonTabs.onStackWillChange prior to emitting this.ionTabsWillChange.

    I have this fix ready on my fork branch and would be happy to open a PR!

  2. thetaPC commented on Oct 30, 2026

    @thetaPC
    Contributor

    Thanks for the report!

    Verified on 9.0.10. This happens when ion-tabs renders after the router has already activated the tab route, for example when ion-tabs sits behind an @if, or when the tabs page is rendered by Angular's <router-outlet> instead of ion-router-outlet. In that case the inner outlet activates the first tab while IonTabs is still in its first change detection, so ionTabsWillChange fires before IonTabs has its outlet reference. Anything that reads outlet in that first handler throws, and so does getSelected(), which uses it internally.

    Workarounds, both verified on 9.0.10:

    • If you don't need the "before" timing, do the work in ionTabsDidChange, where outlet is set from the first event onwards.
    • If it has to run on ionTabsWillChange, defer the read by a microtask:
    willChange(tabs: IonTabs) {
      queueMicrotask(() => console.log(tabs.outlet.nativeEl.children));
    }

    If you'd like to open a PR for it, we'd be happy to review it. One approach that worked in our testing is to pass the #outlet template ref into onStackWillChange from the ion-router-outlet binding in the IonTabs template, and set outlet from it before emitting. The standalone and lazy IonTabs share the same template, so both need the change.

    Tracked as a bug in @ionic/angular.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions