Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ng-template gets rendered twice during hydration (when anchor comment node is not projected) #50543

Open
michael-hein opened this issue Jun 1, 2023 · 13 comments
Assignees
Labels
area: core Issues related to the framework runtime core: hydration P4 A relatively minor issue that is not relevant to core functions
Milestone

Comments

@michael-hein
Copy link

Which @angular/* package(s) are the source of the bug?

platform-browser

Is this a regression?

No

Description

when hydration is enabled and a component uses ngTemplateOutlet to render a template passed from a parent e.g. via ContentChild then its getting rendered twice for the duration of the hydration.

So it looks the like this if 'abc' is the template content
received from server
abc

during hydration
abc abc <-- this looks sometimes a bit ugly

after hydration
abc

So is it possible that the passed template gets just once rendered during the hydration process?

The relevant code parts for reprodruction:

<app-shell>
  <ng-template #customTemplate>
    <p>template</p>
  </ng-template>
</app-shell>
@Component({
  selector: 'app-shell',
  template: `<ng-container
    *ngIf="customTemplate"
    [ngTemplateOutlet]="customTemplate"
  ></ng-container>`,
  styles: [],
})
export class ShellComponent {
  @ContentChild('customTemplate', { static: true })
  customTemplate: TemplateRef<any> | null = null;
}

Please provide a link to a minimal reproduction of the bug

https://github.com/michael-hein/hydration-ng-template

Please provide the exception or error you saw

N/A

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 16.0.4        
Node: 18.16.0
Package Manager: npm 8.19.3
OS: win32 x64

Angular: 16.0.4
... animations, cli, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, platform-server
... router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1600.4
@angular-devkit/build-angular   16.0.4
@angular-devkit/core            16.0.4
@angular-devkit/schematics      16.0.4
@nguniversal/builders           16.0.2
@nguniversal/express-engine     16.0.2
@schematics/angular             16.0.4
rxjs                            7.8.1
typescript                      4.9.5

Anything else?

No response

@dylhunn dylhunn added area: core Issues related to the framework runtime core: hydration labels Jun 1, 2023
@ngbot ngbot bot modified the milestone: needsTriage Jun 1, 2023
@dylhunn
Copy link
Contributor

dylhunn commented Jun 1, 2023

(@jessicajaniuk and @AndrewKushnir for visibility.)

@AndrewKushnir AndrewKushnir added the P4 A relatively minor issue that is not relevant to core functions label Jun 1, 2023
@ngbot ngbot bot modified the milestones: needsTriage, Backlog Jun 1, 2023
@AndrewKushnir
Copy link
Contributor

@michael-hein thanks for reporting the issue and providing a repro, this is very helpful.

I've performed the investigation and found out that what happens is that new nodes are being created for the template instead of reusing existing ones (hence, the duplication), but since you have the setTimeout in the code, it delays the time when hydration kicks off the cleanup operation (is depends on the ApplicationRef.isStable). As a result, the cleanup operation happens a bit later, thus leaving duplicated content visible. During the cleanup run, the SSR'ed content gets removed, since it was not claimed.

I further looked into why existing nodes were not picked up by the hydration and found out that the "anchor" comment node for the projected <ng-template> is considered "disconnected" from the DOM (thus hydration can not find it) and that results in the "regular rendering" codepath. In fact, the "anchor" comment node is really disconnected from the DOM tree, since there was no <ng-content> tag present in the ShellComponent component template. This is not a problem in a simple case, but for more complex scenarios and for hydration purposes, that "anchor" node should be present in live DOM.

To fix the problem, you can add the <ng-content /> tag to the ShellComponent component template. In this case, the comment node would be content-projected (added to the live DOM) and hydration will pick it up.

(adding P4 priority for now and leaving this ticket open to discuss further with the team whether we need to improve this behavior)

@AndrewKushnir AndrewKushnir changed the title ng-template gets rendered twice during hydration ng-template gets rendered twice during hydration (when anchor comment node is not projected) Jun 1, 2023
@michael-hein
Copy link
Author

thanks for the fast response and for looking into it. I really appreciate it :)

Yes the setTimeout was only to better see it, that was rendered twice.

With the <ng-content /> tag it works perfectly. Thank you!

<app-shell>
    <ng-template #customTemplate>
      <p>template</p>
    </ng-template>
  </app-shell>
@Component({
  selector: 'app-shell',
  template: `<ng-content /><ng-container
      *ngIf="customTemplate"
      [ngTemplateOutlet]="customTemplate"
    ></ng-container>`,
  styles: [],
})
export class ShellComponent {
  @ContentChild('customTemplate', { static: true })
  customTemplate: TemplateRef<any> | null = null;
}

@CarlosTorrecillas
Copy link

@AndrewKushnir I have tried the <ng-content /> without success. Perhaps I am not doing something wrong. In my scenario I have

<ng-template #template>
    <ng-container *ngIf="googleBusiness">
        <li>
            Google Business info 1
        </li>
        <li>
            Google Business info 2
        </li>
    </ng-container>
</ng-template>

And my component looks like:

export class GoogleBusinessComponent implements OnInit {
    @Input() googleBusiness: IGoogleBusiness;
    @ViewChild('template', { static: true }) template: TemplateRef<unknown>;

    constructor(private viewContainerRef: ViewContainerRef) { }
      
    ngOnInit() {
        this.viewContainerRef.createEmbeddedView(this.template);
        this.viewContainerRef.element.nativeElement.remove();
    }
}

Essentially I want to remove the component tag from being rendered since that component adds two

  • items to an existing list. I have added the <ng-content /> tag inside the ng-template, and outside. Do I have to do anything else to prevent the component being rendered twice?

  • @AndrewKushnir
    Copy link
    Contributor

    @CarlosTorrecillas the problem that you describe might be different from the one reported here. Could you please try to update your app to v16.1.1 (most recent at this moment) and test the behavior once again? If the problem still exists, please create a minimal repro (in a form of a GitHub repository with a new ng new app) and submit a new ticket, we'll take a look. Thank you.

    @CarlosTorrecillas
    Copy link

    Hi @AndrewKushnir , I do have version 16.1.1 and tried different things but the error seems to be there. I will try to get a sample repo with the issue tomorrow and will submit the defect accordingly

    @CarlosTorrecillas
    Copy link

    Good morning @AndrewKushnir , I have created #50813 with all the repro steps in it. Let me know if you need further info. Thanks!

    @jwittekind
    Copy link

    +1
    Any news on this topic?
    I'm using angular 17 and in my case i'm using content directives:

            <ng-container *ngTemplateOutlet="content?.templateRef || defaultContentTemplate; context: context" />
            <ng-template #defaultContentTemplate>
                    <!-- This content is duplicate when rendered by ssr  -->
            </ng-template>        
    

    @JeanMeche
    Copy link
    Member

    @jwittekind Could you provide a working repro on a github repo ? This basic example without its context doesn't seem to reproduce the issue.

    @DjordjeDursun
    Copy link

    I had the same problem, and it was related to change detection, i fixed it by adding "changeDetection: ChangeDetectionStrategy.OnPush,", to "@component({})" for all of my components

    @DjordjeDursun
    Copy link

    @michael-hein

    @jwittekind
    Copy link

    I had the same problem, and it was related to change detection, i fixed it by adding "changeDetection: ChangeDetectionStrategy.OnPush,", to "@component({})" for all of my components

    I can not confirm that adding "changeDetection: ChangeDetectionStrategy.OnPush," solves the problem for me

    @chriswoodie
    Copy link

    I struggle to understand why this is considered a "minor issue" when ng-template is a very core part of Angular. I've come across this issue many times now in a short space of time which either forces us to skip hydration entirely for a large part of the pages (since it's usually fairly large components), or come up with less preferable workarounds...

    Please just fix the issue.

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    area: core Issues related to the framework runtime core: hydration P4 A relatively minor issue that is not relevant to core functions
    Projects
    None yet
    Development

    No branches or pull requests

    10 participants