#Component being initialized that's not rendered (exluded by *ngIf)

12 messages · Page 1 of 1 (latest)

coral hemlock
#

TL;DR: I have a child component that is excluded by *ngIf, but when the parent component loads, the "excluded" component gets constructed/initialized (but not rendered).

Overview:

  • Let's refer to the parent component as ArticleComponent
  • Let's refer to the child components as BlogArticle and DocumentationArticle
  • ArticleComponent calls a back-end API which returns a json doc that either has a blog or documentation property set (but never both, one is always undefined)
  • *ngIf in ArticleComponent renders either the BlogArticle or DocumentationArticle template

Behavior:
If the current child is the DocumentationArticle, and I navigate to a blog url (via a [RouterLInk]), through logging I observe that the DocumentationArticle's constructor and ngOnInit methods are being called, even though the template is never rendered.

This describes in more detail how the code is structured:

  • Use navigates to a url in the app
  • Routing matches ArticleComponent
  • ArticleComponent subscribes to Router.url and determines what back-end resource needs to be queried, then pushes that resource URI into an NgRx store
  • NgRx effect listens for that action, calls the back-end API, and pushes the loaded document to the store
  • ArticleComponent is subscribed to the NgRx store, and when a new document is pushed, async pipe renders one of the two child components that are each guarded by *ngIf

This is only concerning because ngOnInit in the DocumentationComponent calls backend services for other data, which are needlessly being invoked (aside from when they should).

What should I be looking for as I dig into my code? Ultimately I'd like to learn what bad behaviors I'm introducing that do not play well with the angular's cycles short of being told exactly what the problem is 🙂

#

Parent component (some parts removed, but happy to post):

@Component({...})
export class ArticleComponent implements OnInit, OnDestroy {
  constructor(
    private readonly route: ActivatedRoute,
    private readonly store: Store<AppState>,
    private readonly title: Title){
    console.log("Constructing article");
  }

  private destroyed$ = new Subject<any>();
  
  myDocument$!: Observable<DocumentModel | null>;

  ngOnInit(): void {
    // Pump routes to the store where they are handled by effects
    this.route.url.pipe(
      takeUntil(this.destroyed$),
      map(segments => `docs/${segments.join('/')}`),
      distinctUntilChanged()
    ).subscribe(uri => this.store.dispatch(new LoadDocumentUri(uri)));

    // Receive documents that have been loaded and transformed
    this.myDocument$ = this.store.select(state => state.document)
      .pipe(
        takeUntil(this.destroyed$),
        map(state => state.model)
      );
  }

  ngOnDestroy = () => this.destroyed$.next(0);
}

#

Article component:

<div *ngIf="myDocument$ | async as document"
     class="flex main-body"
     tsMediaBreaks spans="sm=..1100;md=1100..1600;lg=1600.." prefix="article-mq">

    <div *ngIf="myDocument$ | async as model">
        <app-blog-article *ngIf="model.blogArticle" [model]="model.blogArticle"></app-blog-article>
        <app-doc-article *ngIf="model.docArticle" [model]="model.docArticle"></app-doc-article>
    </div>

</div>
rancid cargo
#

Don't .subscribe to get values out of an Observable in Angular. Use the AsyncPipe.
Don't use ngOnInit unless you are using @Input() or if you are .subscribing to WRITE with an Observable because it would be awkward to use the AsyncPIpe. But generally you should use the AsyncPipe whenever you can.

#

takeUntil goes last in the pipe when using it to unsubscribe. Which you shouldn't be here.

rigid cloud
#

Are you sure that your state.document's properties ever have a falsy value?

coral hemlock
rancid cargo
#
@Component({})
export class ArticleComponent {
  public readonly myDocument$: Observable<DocumentModel | null>;

  constructor(
    private readonly route: ActivatedRoute, 
    private readonly store: Store<AppState>,
  ) {
    this.myDocument$ = this.route.url.pipe(
      map((segments: UrlSegment): string => `docs/${ segments.join('/') }`),
      distinctUntilChanged(),
      tap((url: string): void => {
        this.store.dispatch(new LoadDocumentUri(uri));
      }),
      switchMap(() => this.store.select((state) => state.document)),
      map((state: AppState): DocumentModel | null => state.model),
    );
  }
}

But I must say using this.route.url here is an odd decision. Feels like you should be using Params

coral hemlock
#

ahh very elegant

#

OK I'll play around with that. As far as odd decisions, yeah I'm new-ish to Angular so I'm sure I've done many odd or even blatantly wrong things

rancid cargo
#

Remember, all code from the internet has bugs. Including mine above. Treat it like pseudo code, not production quality.

coral hemlock
#

out of curiosity, why does takeUntil() come last in the pipeline?