angulartypescriptrxjsobservablesubject

RxJs operator combineLatest doesn't catch the last emitted value


For some reason combineLatest doesn't catch the last emitted value from Subject.

service.ts

const BLOCKS = [
  {
    name: 'block 1',
    id: 1,
  },
  {
    name: 'block 2',
    id: 2,
  },
];

@Injectable({
  providedIn: 'root',
})
export class Service {
  blocks = BLOCKS;
  blocksSubject = new Subject<any>();
  blocks$ = this.blocksSubject.asObservable().pipe(
    map((blocks) => blocks.map((block) => this.validation(block))),
    map((blocks) => blocks.map((block) => this.asyncValidation(block))),
    switchMap((blocks) => combineLatest(blocks))
  );

  addBlocks() {
    this.blocksSubject.next(BLOCKS);
  }

  removeBlock(id: number) {
    this.blocks = this.blocks.filter((block) => block.id !== id);
    this.blocksSubject.next(this.blocks);
  }

  asyncValidation(block) {
    return of({ ...block, asyncValid: true });
  }

  validation(block) {
    return { ...block, valid: true };
  }
}

Parent component

export class AppComponent {
  blocks$: Observable<any>;
  constructor(private readonly service: Service) {
    this.blocks$ = this.service.blocks$;
  }

  add() {
    this.service.addBlocks();
  }
}

Child component

@Component({
  selector: 'hello',
  template: `<ng-container *ngFor="let block of blocks">
                <div class="container">
                  <div> Block name: {{ block.name }} </div>
                  <div> Block id: {{ block.id }} </div>
                  <div *ngIf="block.valid"> valid: {{ block.valid }} </div>
                  <div *ngIf="block.asyncValid"> asyncValid: {{ block.asyncValid }} </div>
                  <button (click)="remove(block.id)">Remove</button>
                </div>
              </ng-container>`,
  styles: [`h1 { font-family: Lato; } .container { line-height: 18px; width: 100px; display: flex; flex-direction: column; padding: 10px;}`]
})
export class HelloComponent  {
  @Input() blocks: any;

  constructor(private readonly service: Service) {}

  remove(id) {
    this.service.removeBlock(id);
  }
}

Here is the stackBlitz example: https://stackblitz.com/edit/angular-ivy-db7viw?file=src/app/hello.component.ts

When I remove asyncValidation - everything works fine. With asyncValidation blocks$ observable receives new values, except the last one, so last is item never removed.


Solution

  • Good question. :-)

    When you remove the last block combineLatest does not emit because the array of (validated) block observables is empty.

    Solution: fallback to an empty list.

    blocks$ = this.blocksSubject.asObservable().pipe(
        switchMap((blocks) => {
          if (blocks.length) {
            return combineLatest(
              blocks.map((block) => this.asyncValidation(block))
            )
            .pipe(defaultIfEmpty([])); // <-- fallback to empty list
          }
          return of(blocks);
        }),
        map((blocks) => blocks.map((block) => this.validation(block)))
    );
    

    Or, alternatively, start with an empty array:

    return combineLatest(
                  blocks.map((block) => this.asyncValidation(block))
                )
                .pipe(startWith<any>([]));
    

    Stackblitz