Skip to content

Dynamic components aren't hydrated when appModule bootstrap AppComponent #51535

Closed

Description

@hiepxanh

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

core

Is this a regression?

No

Description

After one month chasing this issue. I think I can finally reach it. @AndrewKushnir

Example 1:

link: https://codesandbox.io/p/sandbox/unruffled-dawn-dngll7

Previously, this issue #51157 does show that dynamic component aren't hydrated and his example give 2 instance, because he have bootstrap on app.module.ts and it show 2 instance:
image

Example 2:

link: https://codesandbox.io/p/sandbox/relaxed-feather-27fv8y?file=/src/app/app.module.ts:1,1
I accident commend on bootstrap row in app.module.ts and it does not show error but give expect answer:
image
After that I think we fix that issue and case close

Example 3:

link: https://codesandbox.io/p/sandbox/sharp-gagarin-j9ycmg?file=/src/app/app.component.ts:24,14

I still get the flick and it still a really annoy issue. So I decide to reproduce my closest behavior on my production repo. So it little different but basicly same with example 2. Just only one think, I did not accident commend and now it alway render browser instead of reuse variable hydrated from server.

image

right now I have the same issue with the new reproduce with createComponent function. And I realize that problem come from bootstrap: [AppComponent] is open or not. I think it is important and require because my project instant dead if I commend this.

Please provide a link to a minimal reproduction of the bug

https://codesandbox.io/p/sandbox/sharp-gagarin-j9ycmg?file=/src/app/navbar.component.ts:8,26

Please provide the exception or error you saw

No response

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

"dependencies": {
    "@angular/animations": "^16.2.0",
    "@angular/common": "^16.2.0",
    "@angular/compiler": "^16.2.0",
    "@angular/core": "^16.2.0",
    "@angular/forms": "^16.2.0",
    "@angular/platform-browser": "^16.2.0",
    "@angular/platform-browser-dynamic": "^16.2.0",
    "@angular/platform-server": "^16.2.0",
    "@angular/router": "^16.2.0",
    "@nguniversal/express-engine": "^16.1.3",
    "express": "^4.15.2",
    "rxjs": "~7.8.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.13.0"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^16.1.1",
    "@angular/cli": "~16.2.0",
    "@angular/compiler-cli": "^16.2.0",
    "@nguniversal/builders": "^16.1.2",
    "@types/express": "^4.17.0",
    "@types/jasmine": "~4.3.0",
    "@types/node": "^14.15.0",
    "jasmine-core": "~4.6.0",
    "karma": "~6.4.0",
    "karma-chrome-launcher": "~3.2.0",
    "karma-coverage": "~2.2.0",
    "karma-jasmine": "~5.1.0",
    "karma-jasmine-html-reporter": "~2.0.0",
    "typescript": "~5.0.2"
  }

Anything else?

No response

Activity

  1. modified the milestone: needsTriage on Aug 28, 2023
  2. AndrewKushnir commented on Aug 28, 2023

    @AndrewKushnir
    Contributor

    @hiepxanh do you see any difference in behavior if you use the bootstrapApplication function and a standalone component to bootstrap an app?

  3. hiepxanh commented on Aug 29, 2023

    @hiepxanh
    ContributorAuthor

    Yes I do prepare a standalone version. forgot to mention

    https://codesandbox.io/p/github/hiepxanh/angular-hydration-flick/application?workspaceId=d385b77c-499a-43e2-a628-69ac00525375

    I do read again about your test and I think maybe the different cause this issue, but the test pass but the app is not

    it('should hydrate dynamically created components using root component as an anchor', async () => {
    @component({
    standalone: true,
    imports: [CommonModule],
    selector: 'dynamic',
    template: <span>This is a content of a dynamic component.</span> <b *ngIf="isServer">This is a SERVER-ONLY content</b> <i *ngIf="!isServer">This is a CLIENT-ONLY content</i>,
    })
    class DynamicComponent {
    isServer = isPlatformServer(inject(PLATFORM_ID));
    }

        @Component({
          standalone: true,
          selector: 'app',
          template: `
            <main>Hi! This is the main content.</main>
          `,
        })
        class SimpleComponent {
          vcr = inject(ViewContainerRef);
    
          ngAfterViewInit() {
            const compRef = this.vcr.createComponent(DynamicComponent);
            compRef.changeDetectorRef.detectChanges();
          }
        }
    
        const html = await ssr(SimpleComponent);
        let ssrContents = getAppContents(html);
    
        expect(ssrContents).toContain('<app ngh');
    
        resetTViewsFor(SimpleComponent, DynamicComponent);
    
        const appRef = await hydrate(html, SimpleComponent);
        const compRef = getComponentRef<SimpleComponent>(appRef);
        appRef.tick();
    
        ssrContents = stripExcessiveSpaces(stripUtilAttributes(ssrContents, false));
    
        // We expect to see SERVER content, but not CLIENT.
        expect(ssrContents).not.toContain('<i>This is a CLIENT-ONLY content</i>');
        expect(ssrContents).toContain('<b>This is a SERVER-ONLY content</b>');
    
        const clientRootNode = compRef.location.nativeElement;
    
        await whenStable(appRef);
    
        const clientContents =
            stripExcessiveSpaces(stripUtilAttributes(clientRootNode.outerHTML, false));
    
        // After the cleanup, we expect to see CLIENT content, but not SERVER.
        expect(clientContents).toContain('<i>This is a CLIENT-ONLY content</i>');
        expect(clientContents).not.toContain('<b>This is a SERVER-ONLY content</b>');
      });
    
  4. AndrewKushnir commented on Aug 29, 2023

    @AndrewKushnir
    Contributor

    @hiepxanh thanks for the reply. I've tried opening the page using the link, but got the Unable to access this workspace error. Could you please try to create a repro using a GitHub repository (same code that you use in CodeSandbox, just in a repository) with the most recent version of Angular, so that we can take a look and try to run it? Please also include a bit more information on what is the expected vs actual behavior, it'd help to speed up the investigation. Thank you.

  5. hiepxanh commented on Aug 29, 2023

    @hiepxanh
    ContributorAuthor

    Yes I'm using https://github.com/hiepxanh/angular-hydration-flick sorry to meantion that, I though use codesandbox will help you faster :D

  6. hiepxanh commented on Aug 29, 2023

    @hiepxanh
    ContributorAuthor

    @AndrewKushnir I also do invite you to that repo for more convenient. You can use main branch as app.module and application branch for standalone version
    image

  7. AndrewKushnir commented on Aug 29, 2023

    @AndrewKushnir
    Contributor

    @hiepxanh thanks for sharing a repro 👍

    I've performed some debugging and found out that the problem was caused by the following code: <div [innerHTML]="sanitizedContent"></div>. What happens is that Angular hydration finds and hydrates the <div>, but after that the innerHTML binding just overrides everything inside that <div>, removing the content that was server-side rendered.

    The use of innerHTML falls under the "direct DOM manipulation" category, where Angular doesn't know about the elements being created, thus it's unable to hydrate that segment (see more info about direct DOM manipulation in this guide, the innerHTML is listed there too).

    As a possible solution for dynamic component creation (instead of using innerHTML), you can consider using the NgComponentOutlet, which would hydrate better.

  8. hiepxanh commented on Aug 30, 2023

    @hiepxanh
    ContributorAuthor

    great, I already consider ngComponentOutlet and try to achive it and I see that it is undefined too as innerHTML. Since ngComponentOutlet is nothing but just an other way to project content. But I findout that I can achive it with transferState and split render in server and skip on browser. I'm going to create a PR to make a docs about this but I will just create it :D

  9. hiepxanh commented on Aug 30, 2023

    @hiepxanh
    ContributorAuthor

    for note purpose, with ElementRef.nativeElement, innerHTML can survive after hydration. After that use check to render only on server can achive that skip flick

    @Component({
      standalone: true,
      selector: 'ngx-dynamic',
      template: ``,
    })
    export class NgxDynamic {
      platformId = inject(PLATFORM_ID);
      @Input() content!: string;
      @Input() context!: any;
      @Input() componentData!: any;
      elementRef = inject(ElementRef);
      applicationRef = inject(ApplicationRef);
      document = inject(DOCUMENT);
      domSanitizer = inject(DomSanitizer);
      cd = inject(ChangeDetectorRef);
      isServer = isPlatformServer(this.platformId);
      ngOnInit() {
        if (this.isServer) {
          this.elementRef.nativeElement.innerHTML = (
            this.domSanitizer.bypassSecurityTrustHtml(this.content) as any
          )['changingThisBreaksApplicationSecurity'];
          console.log('at server', this.content);
        } else {
          console.log('at client', this.content);
        }
      }
    
      ngAfterViewInit() {
        if (this.isServer) {
          console.log('2', this.platformId, this.isServer, this.context);
          this.render();
        }
      }
    
      async render() {
        const hostElement = this.document.querySelector(
          this.componentData.selector
        );
        if (hostElement && this.componentData) {
          const compRef = createComponent(this.componentData.component, {
            hostElement,
            environmentInjector: this.applicationRef.injector,
          });
          compRef.setInput('data', this.context.data);
          this.applicationRef.attachView(compRef.hostView);
          compRef.changeDetectorRef.detectChanges();
          this.cd.detectChanges();
        }
      }
    }
    
    @Component({
      standalone: true,
      selector: 'app-navbar',
      template: `<p>navbar data: {{ data }}</p>`,
    })
    export class NavbarComponent {
      @Input() data!: any;
    }
    
    @Component({
      selector: 'app-root',
      template: `<p>app-root: {{ platform }}</p>
        <ngx-dynamic [content]="content" [context]="context" [componentData]="componentData"></ngx-dynamic>`,
      imports: [NgxDynamic],
      standalone: true,
    })
    export class AppComponent {
      platform = inject(PLATFORM_ID);
      context = { data: this.platform };
      content = `<app-navbar></app-nabar>`;
      componentData = {
        component: NavbarComponent,
        selector: 'app-navbar',
      };
    }

    to see that example https://github.com/hiepxanh/angular-hydration-flick/tree/application

    result will be this
    image

  10. angular-automatic-lock-bot commented on Sep 30, 2023

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  11. locked and limited conversation to collaborators on Sep 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions