Search code examples
angularrxjssignalsngzone

rxjs.Interval subscription not triggering changes in Angular


I am playing around with RxJS and Angular 19.1 and I run into a very strange behavior:

interval.component.ts

export class IntervalComponent {
  count: string[] = [];
  
  constructor (private cdr: ChangeDetectorRef) {
    const secondsCounter = interval(1000);
    const subscription = secondsCounter.subscribe (n => {
      this.count = [...this.count, `It's been ${n} seconds since subscribing!`]
      // I cannot understand why I need to trigger a change detection here as I am changing the value by reference.
      // Either way, if I don't any change gets detected and both the `@for` and the `| json` pipe stays empty.
      this.cdr.detectChanges();
      if (n === 10) subscription.unsubscribe();
    });
  }
}

interval.component.html

<h1>Intervals</h1>
<ul>
  @for (c of count; track c) {
    <li>{{ c }}</li>
    <!-- Here it's supposed to add one value per second, but all of them are displayed from the beginning as if they were already loaded. -->
  }
</ul>

{{ count | json}}
<!-- That's why I added this line, to check what was happening. -->
<!-- Here, all the values are displayed as they were supposed to, adding one by one every second. -->

Everything super straightforward. No changeDetection is the default - JIC.

I know I can try with signals, actually I have tried. Then the detectChanges is not needed, but the behavior in .html is the same. The values on @for are displayed all at once.

Any ideas on why is this happening?


Solution

  • I tried your example with SSR and it behaved as you said:

    Here it's supposed to add one value per second, but all of them are displayed from the beginning as if they were already loaded.

    This is due to having SSR enabled, so the content get's displayed because it was rendered on the server (That is why you see the page taking a lot of time to load 10s) and then immediately showing the output.

    When it get's displayed on the browser hydration happens and the bottom content is loaded again, but the top content has already rendered.

    To solve this wrap your TS code inside the isPlatformBrowser(this.platformId) if condition, so that it runs only on the browser and it will start working fine.

    import { afterNextRender, ChangeDetectorRef, Component, inject, PLATFORM_ID } from '@angular/core';
    import { isPlatformBrowser, JsonPipe } from '@angular/common';
    import { interval } from 'rxjs';
    
    @Component({
      selector: 'app-root',
      imports: [JsonPipe],
      templateUrl: './app.component.html',
      styleUrl: './app.component.scss',
    })
    export class AppComponent {
      count: string[] = [];
      platformId = inject(PLATFORM_ID);
    
      constructor(private cdr: ChangeDetectorRef) {
        if (isPlatformBrowser(this.platformId)) {
          const secondsCounter = interval(1000);
          const subscription = secondsCounter.subscribe((n: any) => {
            this.count = [...this.count, `It's been ${n} seconds since subscribing!`]
            // I cannot understand why I need to trigger a change detection here as I am changing the value by reference.
            // Either way, if I don't any change gets detected and both the `@for` and the `| json` pipe stays empty.
            // this.cdr.detectChanges();
            if (n === 10) subscription.unsubscribe();
          });
        };
      }
    }