Angular: ngAfterContentInit executed twice

Created on 30 Jun 2017  路  7Comments  路  Source: angulardart/angular

I'm using directives to mark components as content items for example, *contentItem and then using that as a way for @ContentChildren(ContentItemDirective) to pick up only those components for rendering.

The problem is, those children marked with *contentItem have their ngAfterContentInit executed twice even though the component is only rendered once. Since we start event listeners on an EventBus during ngAfterContentInit, we end up having duplicate event listeners on the bus causing things to happen twice. For example, a toast popup saying busy saving / done saving now gets display twice since we have two listeners created on the bus. Once the component gets destroyed, both listeners are also destroyed at the same time.

The suggestion was that in dev mode, things are checked twice and in production it doesn't check for side-effects. I've experienced the same behavior in production as well, so not sure if that's related. Some other suggestions said that if there's a template error, ngAfterContentInit will get executed twice. I stripped down the HTML to the bare minimum and can't see anything that's blatantly wrong in the template.

Currently on Dart 1.24.2 and Angular 3.1.0

I've submitted a very detailed StackOverflow question:
https://stackoverflow.com/questions/44832253/stop-ngaftercontentinit-from-executing-twice

Reproduced in Dartium on both Windows10 and OSX as well as on Chrome (latest), I don't think this is browser related.

I've published a sample project here which will reproduce the problem:
https://github.com/jVaaS/stackoverflow/tree/master/ngaftercontentinit

Run the project with pub serve and observe in the console that >>> ngAfterContentInit: Demo is printed twice.

In the meantime, is there a workaround for this? Maybe another Angular hook where we can wire in our EventBus where it won't get wired in twice?

as intended

Most helpful comment

This doesn't seem like a bug.

Here is the generated code for demo.dart:
https://gist.github.com/matanlurey/f311d90053e36cc09c6e28f28ab2d4cd

  void detectChangesInternal() {
    bool firstCheck = identical(this.cdState, ChangeDetectorState.NeverChecked);
    final _ctx = ctx;
    if (!import8.AppViewUtils.throwOnChanges) {
      dbg(0, 0, 0);
      if (firstCheck) {
        _Demo_0_2.ngAfterContentInit();
      }
    }
    _compView_0.detectChanges();

I was suspicious, so I changed your print message to include the hashCode:

    @override
    ngAfterContentInit() {
        print(">>> ngAfterContentInit: Demo $hashCode");
    }

Then I saw the following:

> ngAfterContentInit: Demo 516575718
> ngAfterContentInit: Demo 57032191

Which means two _diffferent_ instances of demo were alive, not just one emitting twice. I then added StackTrace.current to the constructor of demo to get a stack trace:

Demo() {
        print('Created $this:$hashCode');
        print(StackTrace.current);
    }

And got:

#0      Demo.Demo (package:bugdemo/demo.dart:11:20)
#1      ViewBuggyApp1.build (package:bugdemo/buggy-app.template.dart:136:21)
#2      AppView.create (package:angular2/src/core/linker/app_view.dart:180:12)
#3      DebugAppView.create (package:angular2/src/debug/debug_app_view.dart:73:26)
#4      TemplateRef.createEmbeddedView (package:angular2/src/core/linker/template_ref.dart:27:10)
#5      ViewContainer.createEmbeddedView (package:angular2/src/core/linker/view_container.dart:86:43)

Which in turn said your component was being created by BuggyApp's template:

        <standard-layout>
            <demo *contentItem></demo>
        </standard-layout>

Closer. Kept digging.

I commented out ContentItemDirective, and saw Demo was now created once. I imagine you expected it not to be created at all due to *contentItem, so kept digging - and finally figured it out.

Your StandardLayout component creates templates:

        <div *ngFor="let item of contentItems ?? []">
            <template [ngTemplateOutlet]="item.template"></template>
        </div>

But so does your ContentItemDirective via _ComponentResolver_. I imagine you did not intend this. So I'd figure out what you were trying to do and address your issue by removing creating the component in one of these places.

All 7 comments

It's like you have an exception occurring somewhere:

https://github.com/dart-lang/angular/blob/master/doc/crash_detection.md

I've stripped away almost everything, there's not much code left where it can fail. How would one go about detecting such an exception?

@matanlurey , I'm just following up, does needs info mean you're waiting for more info from me?
If it's a mechanism to reproduce the problem that's outstanding, I've added that info in the github repo: https://github.com/jVaaS/stackoverflow/tree/master/ngaftercontentinit

@janvladimirmostert Taking a look!

This doesn't seem like a bug.

Here is the generated code for demo.dart:
https://gist.github.com/matanlurey/f311d90053e36cc09c6e28f28ab2d4cd

  void detectChangesInternal() {
    bool firstCheck = identical(this.cdState, ChangeDetectorState.NeverChecked);
    final _ctx = ctx;
    if (!import8.AppViewUtils.throwOnChanges) {
      dbg(0, 0, 0);
      if (firstCheck) {
        _Demo_0_2.ngAfterContentInit();
      }
    }
    _compView_0.detectChanges();

I was suspicious, so I changed your print message to include the hashCode:

    @override
    ngAfterContentInit() {
        print(">>> ngAfterContentInit: Demo $hashCode");
    }

Then I saw the following:

> ngAfterContentInit: Demo 516575718
> ngAfterContentInit: Demo 57032191

Which means two _diffferent_ instances of demo were alive, not just one emitting twice. I then added StackTrace.current to the constructor of demo to get a stack trace:

Demo() {
        print('Created $this:$hashCode');
        print(StackTrace.current);
    }

And got:

#0      Demo.Demo (package:bugdemo/demo.dart:11:20)
#1      ViewBuggyApp1.build (package:bugdemo/buggy-app.template.dart:136:21)
#2      AppView.create (package:angular2/src/core/linker/app_view.dart:180:12)
#3      DebugAppView.create (package:angular2/src/debug/debug_app_view.dart:73:26)
#4      TemplateRef.createEmbeddedView (package:angular2/src/core/linker/template_ref.dart:27:10)
#5      ViewContainer.createEmbeddedView (package:angular2/src/core/linker/view_container.dart:86:43)

Which in turn said your component was being created by BuggyApp's template:

        <standard-layout>
            <demo *contentItem></demo>
        </standard-layout>

Closer. Kept digging.

I commented out ContentItemDirective, and saw Demo was now created once. I imagine you expected it not to be created at all due to *contentItem, so kept digging - and finally figured it out.

Your StandardLayout component creates templates:

        <div *ngFor="let item of contentItems ?? []">
            <template [ngTemplateOutlet]="item.template"></template>
        </div>

But so does your ContentItemDirective via _ComponentResolver_. I imagine you did not intend this. So I'd figure out what you were trying to do and address your issue by removing creating the component in one of these places.

This is brilliant, you've given me the tools to detect such problems in the future, thanks @matanlurey !

So this content item had to be changed to:

@Component(
    selector: "content-item",
    host: const { '[class.content-item]': "true"}
)
class ContentItem {
    TemplateRef template;
}

Then it works 100%. Had no idea that <template [ngTemplateOutlet] was actually creating an instance.

I have an open bounty on the StackOverflow question that you can claim:
https://stackoverflow.com/questions/44832253/stop-ngaftercontentinit-from-executing-twice

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Tomucha picture Tomucha  路  5Comments

AndreyChernykh picture AndreyChernykh  路  4Comments

4cm4k1 picture 4cm4k1  路  5Comments

matanlurey picture matanlurey  路  4Comments

matanlurey picture matanlurey  路  3Comments