1
votes

I've faced with a wierd case, when the async pipe doesn't work properly until I subscribe to the observable. Sources:

View of the component:

<div *ngIf="loading$ | async" fxLayout="row" fxLayoutAlign="center">
    <span class="spinner">Loading...</span>
</div>

<h1>{{data$ | async}}</h1>

Component:

@Component({
  selector: './tm-fake',
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: './fake.component.html'
})
export class FakeComponent implements OnInit {
  constructor(
    private _fs: FakeService
  ) {}

  loading$ = new BehaviorSubject<boolean>(false);
  data$ = this._fs.data$.pipe(indicateUntilNULL(this.loading$));

  ngOnInit(): void {
    this._fs.requestData();

    //this.data$.subscribe(e => {});
  }
}

Service:

@Injectable()
export class FakeService {
  requestData(): void {
    timer(2000).pipe(map(e => "data")).subscribe(e => this._data.next(e));
  }

  private readonly _data = new BehaviorSubject<string>(null);
  readonly data$ = this._data.asObservable();
}

Operator:

export function indicateUntilNULL<T>(indicator: BehaviorSubject<boolean>): (source: Observable<T>) => Observable<T> {
  return source => source.pipe(
    tap(e => indicator.next(!e))
  );
}

I see the data after 2 sec delay as excpected, but I didn't see a spinner during this delay. But if I uncomment the line this.data$.subscribe(e => {}); it starts to work fine. And I can't find the reason. Any ideas?

NB: loading$ emits correct values even this line is commented.

SOLUTION

Many thanks to @theMayer and his helpful advice. I just want to add the article, where the problem is explained in details. And according this article the most elegant solution is a delay(0) operator:

export function indicateUntilNULL<T>(indicator: BehaviorSubject<boolean>): (source: Observable<T>) => Observable<T> {
  return source => source.pipe(
    delay(0),
    tap(e => indicator.next(!e))
  );
}
2
Why are you initializing with Null? That is not how it is supposed to be usedtheMayer
Can you try just printing {{ loading$ | async }}? Because what you have should work even with onPush.martin
@theMayer Because I don't have data right after page loaded. it should be requested from API. I replaced it with a timer for simplicityDon Tomato
@martin the same - I replaced a spinner with <h1>{{loading$ | async}}</h1> and it shows false all the time without subscription and works as expected with subscription - first it shows true, and then - after delay false.Don Tomato
A boolean cannot logically be anything other than true or false. I would recommend initializing it with a value.theMayer

2 Answers

0
votes

OK, after reviewing your code, I think I understand the question. You are not seeing the updated value of true emitted via the loading$ observable picked up in the DOM.

Here is what your code says will happen:

  1. The objects will be constructed, and the behavior subject _data will be initialized with a null value.

  2. The Angular template will render in and go through change detection. It will pick up false for the loading value and null for data. The loading spinner will not spin.

  3. Later in the change detection cycle, the async pipe will trigger the true value to be emitted by loading. This happens synchronously; since change detection was set to OnPush, I suspect this change is going to be missed because there is already a change detection cycle in progress when the value was set (normally, this might be caught by the regular change detection strategy and raised as an error).

  4. Then your data emits after the two second delay.

Note that, when using regular change detection, this sort of a situation will result in a ExpressionChangedAfterItHasBeenCheckedError. The correct approach is to ensure the internal consistency of your component prior to the change detection cycle rather than do things during change detection which trigger further changes.

The quick answer is to set your "loading" value to true initially, since the code will always try to load data when it initializes.

-2
votes

You need share() Here's my working example:

public $route!: Observable<{}>;

constructor(
    private route: ActivatedRoute
) { }

public ngOnInit(): void {
    this.$route = this.route.data.pipe(share());
}

Then in the view

<ng-container *ngIf="$route | async as route; else loading">
    ...
</ng-container>