See RxJS counterparts of array methods in action

Reading Time: 4 minutes

Loading

Introduction

This is day 7 of Wes Bos’s JavaScript 30 challenge where I am going to apply some RxJS counterparts of array methods to streams to obtain results.

In this blog post, I create observables from arrays and use some, every, filter, find and findIndex operators on RxJS streams to get some answers to some questions.

let's go

Create a new Angular project in workspace

ng generate application day7-array-cardio-part2

Create RxJS custom operator to do Array.some

RxJS counterparts of array methods include every, filter, find and findIndex, and some is not one of them. Therefore, I create a custom-operators directory to define a custom operator for Array.some.

The implementation of the custom operator can be found in some.operator.ts

// some.operator.ts

import { Observable, find, map, defaultIfEmpty } from 'rxjs';

export function some<T>(predicate: (item: T) => boolean) {
    return function (source: Observable<T>) {
        return source.pipe(
            find(item => predicate(item)),
            map(c => !!c),
            defaultIfEmpty(false),
        )
    }
}
  • the operator accepts a predicate that returns a boolean result
  • the source stream uses RxJS find to look for the first item that satisfies the predicate
  • emits the first item and use map to coerce object to boolean value
  • when source stream does not emit any item, defaultIfEmpty defaults the result to false

Define RxJS streams and emit them to arrays

src/app
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── custom-operators
│   └── some.operator.ts
└── interfaces
    ├── comment.interface.ts
    └── person.interface.ts

Let’s construct some streams and then use RxJS counterparts of array methods. Persons is an array of object and I convert it into an observable using from operator.

// app.component.ts

persons = [
    { name: 'Wes', year: 1988 },
    { name: 'Mary', year: 2006 },
    { name: 'George', year: 2009 },
    { name: 'Kait', year: 1986 },
    { name: 'Irv', year: 1970 },
    { name: 'Lux', year: 2015 }
];

people$ = from(this.persons).pipe(
    map((person) => this.calculateAge(person)),
    shareReplay(this.persons.length),
);

private calculateAge(person: PersonNoAge): Person {
   return {
      ...person,
      age: new Date().getFullYear() - person.year
   };
}

shareReplay(this.persons.length) caches all the persons before other streams reuse people$ to test conditions and do filtering. CalculateAge calculates the age of each person to help me verify the results.

Define peopleArray$ to get a persons array when people$ completes

peopleArray$ = this.people$.pipe(toArray());

Next, I construct a similar comments stream that is going to illustrate find and findIndex examples.

// app.component.ts

comments = [
    { text: 'Love this!', id: 523423 },
    { text: 'Super good', id: 823423 },
    { text: 'You are the best', id: 2039842 },
    { text: 'Ramen is my fav food ever', id: 123523 },
    { text: 'Nice Nice Nice!', id: 542328 }
] 

comments$ = from(this.comments).pipe(shareReplay(this.comments.length));
commentsArray$ = this.comments$.pipe(toArray());

Render peopleArray$ and commentsArray$ to visualize the data sets

@Component({
  selector: 'app-root',
  template: `
    <div class="container">
      <section class="people">
        <h1>People</h1>
        <ul *ngIf="peopleArray$ | async as peopleArray">
          <li *ngFor="let p of peopleArray">Name: {{ p.name }}<br/> Year: {{ p.year }}<br/> Age: {{ p.age }}</li>
        </ul>
      </section>
      <section class="comments">
        <h1>Comments</h1>
        <ul *ngIf="commentsArray$ | async as commentsArray">
          <li *ngFor="let p of commentsArray">Id: {{ p.id }}<br/> Text: {{ p.text }}</li>
        </ul>
      </section>
    </div>
  `,
  styles: [`...omitted for brevity...`]
})
export class AppComponent { ...RxJS codes... }

Example 1: Is at least one person an adult (19 or older)?

The question can answer by custom operator, some.

isAdult$ = this.people$.pipe(some(person => this.isAnAdult(person)));

private isAnAdult(person: Person, age = 19): boolean {
   return person.age >= age;
}

The stream finds the first person that is 19 or older and returns a boolean value.

Use async pipe to resolve isAdult$ and display the value

<p>Is Adult (at least one person is 19 or older)? {{ isAdult$ | async }}</p>

Example 2: Is everyone an adult (19 or older)?

allAdults$ = this.people$.pipe(every(person => this.isAnAdult(person)));

The stream validates every person is 19 or older.

Use async pipe to resolve allAdults$ and display the value

<p>All Adults (everyone is 19 or older)? {{ allAdults$ | async }}</p>

Example 3: Who are 19 years old or older?

adults$ = this.people$.pipe(
    filter(person => this.isAnAdult(person)),
    toArray()
); 

<section class="people">
   <h1>Adults</h1>
   <ul *ngIf="adults$ | async as adults">
       <li *ngFor="let p of adults">Name: {{ p.name }}<br/> Year: {{ p.year }}<br/> Age: {{ p.age }}</li>
   </ul>
 </section>

adults$ stream filters persons who are 19 and older. When stream completes, I use toArray to emit a person array in order to render the array elements in inline template.

Example 4: Find comment where id = 823423

comment$ = this.comments$.pipe(find(c => c.id === 823423));

The type of comment$ is Observable<Comment | undefined> because the stream may or may have the id.

<ng-container *ngIf="comment$ | async as comment; else noComment">
     <p>Find comment 823423?</p>
     <p>Id: {{ comment.id }}, text: {{ comment.text }}</p>
</ng-container>

<ng-template #noComment>
   <p>Comment does not exist</p>
</ng-template>

When comment exists, the template displays comment id and comment id. Otherwise, the template displays “Comment does not exist”.

Example 5: Find index of comment where id = 823423

Instead of getting back the comment, I am interested in its index. It is feasible by replacing find with findIndex.

commentIndex$ = this.comments$.pipe(findIndex(c => c.id === 823423));

<p>FindIndex of comment 823423? {{ commentIndex$ | async }}</p>

Final Thoughts

In this post, I show that some array methods have RxJS counterparts and they are used in a similar fashion except the source is an observable. When RxJS counterparts are absent, for example some and sort, I build my own custom operators that work for observable. Ultimately, the results are the same as if the input is an array.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

  1. Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day7-array-cardio-part2
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day7-array-cardio-part2/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30