ngOnInitworks hectic with lazy loading

Hey guys, I’m experiencing a strange issue.

I’m aware that ngOnInit() should only run once, when the component is loaded, and after that only ionViewDidEnter() signals, that the user has navigated to the given component. In spite of this, ngOnInit() gets called (usually?) when a user navigates to a component, regardless if he has been there before.

I have lazy loading configured in the application.
Here is part of the app-routing.module.ts

  {
    path: 'home',
    canActivate: [AuthGuard],
    loadChildren: './home/home.module#HomePageModule',
    resolve: { homeData: HomeResolver },
    data: {
      frame: true,
    },
  },
  {
    path: 'news',
    canActivate: [AuthGuard],
    loadChildren: './news/news.module#NewsPageModule',
    data: {
      frame: true,
    },
  },
  {
    path: 'client-status',
    canActivate: [AuthGuard],
    loadChildren: './client-status/client-status.module#ClientStatusPageModule',
    resolve: { clientStatusData: ClientStatusResolver },
    data: {
      frame: true,
    },
  },

Here is client status page module for example:

const routes: Routes = [
  {
    path: '',
    component: ClientStatusPage
  }
];

@NgModule({
  imports: [
    IonicModule,
    CommonModule,
    AppCoreModule,
    FormsModule,
    PipesModule,
    InlineSVGModule.forRoot(),
    RouterModule.forChild(routes),
  ],
  declarations: [ClientStatusPage]
})
export class ClientStatusPageModule {}

Everything is similar to what we can find in tutorials.

The difference is in maybe in the html structure, and the components providing it.
app-component.html:

<ion-app>

  <app-frame *ngIf="frame; else noframe">
    <ng-container *ngTemplateOutlet="noframe"></ng-container>
  </app-frame>

  <ng-template #noframe>
    <ion-router-outlet></ion-router-outlet>
  </ng-template>

</ion-app>

The frame variable comes from the data provided in the routing

    data: {
      frame: true,
    },

We use this component to render a header and a footer in the application, frame.component.html:

  <ion-header>
    <app-header></app-header>
  </ion-header>

  <div>
    <ng-content></ng-content>
  </div>

  <ion-footer>
    <app-menu></app-menu>
  </ion-footer>

In the footer there is a menu with some anchor tags, pointing to the urls defined in app-routing.module.ts.
For example:

    <a routerLink="/home" routerLinkActive="active">
      Home
    </a>

So the behaviour I’m experiencing, is that from the Home module, if I click on any anchor tag (for example Settings), and load up a module, the settings.page.ts ngOnInit gets called. If I navigate back to Home, the home.page.ts ngOnInit doesn’t get called a 2nd time. But if I navigate to Settings again, the settings.page.ts ngOnInit of gets called again.

Sometimes all of the components trigger ngOnInit upon navigation, sometimes a given component’s ngOnInit gets triggered only if I navigate there from 1-2 particular component, but doesn’t get triggered if I navigate there from a different one.

Sorry for the cloudy explanation, but I’m totally clueless why this happens, hope someone can point me in the right direciton.

Is ngOnDestroy called as well? That’s all I would care about - that those two calls are paired (and, frankly, I write as if I can’t even rely on that).

I checked, it is called consistently. Meaning that when it’s called on a given component, I can expect ngOnInit will be called when I navigate back to it, but if it is not called, ngOnInit won’t be either.

What’s interesting is that I found out it’s building a tree of some sort.

In the footer menu there are anchors with different routes, let’s mark them with A, B, C.

Navigating in alphanumeric order doesn’t destroy components, while navigating backwards does.
So if I navigate A > B > C > A
The call order is
A - ngOnInit
B - ngOnInit
C - ngOnInit
C - ngOnDestroy, B - ngOnDestroy simultaneously

1 Like

Thanks for the detective work. Now let’s help fix your concern - how can we get your app to do what you want it to without over-reliance on lifecycle events?

Well, I use lifecycle events to pull relevant data from an API to a given component.
I could pull all data on startup, then store it in memory, then distribute it to the given component when needed (the component gets created), but I would have preferred to get it in ngOnInit, then store it.

And every time I navigate back, the data is there already.

And another interesting issue is that on some components, when I navigate to /home, the component gets destroyed, home created, then another “ghost” component created in the background, but home is shown on the screen. ngOnInit runs on the background component, if I subscribe to an event in ngOnInit then it will listen to it from now on. If I navigate back to this component, another one gets created, which is destroyed after I navigate to /home, but then another ghost component created, 2 of these are in the background now and listening to events. Repeating this creates n number of background components listening to events (or just simply existing, and possibly creating a memory leak)

So something is really off with these lifecycle events in my app. I can work around it of course, but it’d be nice to know what’s happening, I’m curious too.

I think this is a mistake, because lifecycle events are a strictly client-side framework concern, whereas interaction with a backend is not. Upstream data isn’t going to change just because some user’s device ran low on memory and evicted a page from the cache of a mobile app. Also, the details of when components are recycled or reaped tend to change over time, often not in a well-documented way, so you get mysteriously appearing bugs. Finally, things are also somewhat at the whim of the individual device, so we have inconsistent mysteriously appearing bugs.

So, the following triggers for fetching data from the backend make sense to me:

  • at startup
  • periodically
  • specifically at user request (such as a refresh button)
  • when your app can logically deduce that something has changed (such as a response to a notification)

In conclusion, I deliberately do not want to know the details of what causes lifecycle events to fire or not fire when in a global context, because I don’t want to even subconsciously rely on it. All I use them for is purely local concerns to the component: things it needs to do to get into a sane state at startup that can’t be put into the constructor for technical reasons (such as @ViewChild not being ready or bound properties not being bound yet) or things it needs to do to avoid leaking memory, such as unsubscribing from Observables.