AngularのAsyncPipeを学ぶ


この記事は[Angular Advent Calendar 2022](https://qiita.com/advent-calendar/2022/angular) 20日目の記事です。
AsyncPipeについての学習記録です。


# AsyncPipeとは

まずは公式ページを確認します。

https://angular.io/api/common/AsyncPipe

> The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted. When a new value is emitted, the async pipe marks the component to be checked for changes. When the component gets destroyed, the async pipe unsubscribes automatically to avoid potential memory leaks. When the reference of the expression changes, the async pipe automatically unsubscribes from the old Observable or Promise and subscribes to the new one.

1. `Observable`（や`Promise`）を購読して、最新の値を返す。
2. `Component`が破棄された際に、AsyncPipeは自動的に購読を解除する。
3. 式の参照が変わった際に、AsyncPipeは古い`Observavle`の購読を自動的に解除し、新しい`Observavle`を購読する。

---

`AsyncPipe`を使うと何が嬉しいかというと、

- コンポーネントでの購読解除に関するコードを書かなくて良い
    - 購読解除忘れを心配をしなくて良い（購読解除忘れはメモリリークを引き起こす）
    - コンポーネントのコード量が減りスッキリする
- `Observable`の初期化をコンストラクタで完結させることができる
- readonlyにできる


https://zenn.dev/lacolaco/books/angular-after-tutorial/viewer/subscribe-observables-in-components

2.3個目については私のような初心者レベルではあまり旨味を感じたことがないので、良い例が見つかったらまた書きます。
1個目については、AsyncPipeの導入前・後のコードを比べれば一目瞭然です。

```typescript:user-list.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { UserListService } from '../user-list.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'user-list',
  template: `
    <ul>
        <li *ngFor="let user of users">
    		{{ user.id }} {{ user.first_name }} {{ user.last_name }}
    	</li>
    </ul>
  `,
})
export class UserListComponent implements OnDestroy, OnInit {
  private _onDestroy = new Subject<void>();
  users: User[];

  constructor(private userListService: UserListService) { }

  ngOnInit() {
    this.userListService.getUsers()
      .pipe(takeUntil(this.onDestroy))
      .subscribe(users => {
        this.users = users;
      });
  }

  ngOnDestroy() {
    this._onDestroy.next(null);
  }

}
```
```typescript:user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { UserListService } from '../user-list.service';

@Component({
  selector: 'user-list',
  template: `
    <ul>
        <li *ngFor="let user of users$ | async">
    		{{ user.id }} {{ user.first_name }} {{ user.last_name }}
    	</li>
    </ul>
  `,
})
export class UserListComponent implements OnInit { // だいぶスッキリした
  readonly users$: Observavle<User[]>;

  constructor(private userListService: UserListService) {
    this.users$ = this.userListService.getUsers();
  }

  ngOnInit() {
  }
}
```
コンポーネント内で購読解除に関する記述がまるまる消えてだいぶスッキリしましたね。


しかしそんなAsyncPipeにも辛い点があります。

# 初期値null問題

https://www.youtube.com/watch?v=s4KvTz9eNNg

> ストリームの時点では絶対nullは入ってないのに、AsyncPipeを使うとその子供ではnullを気にしないといけなくなる。

例えば下記のような場合、コンパイル時点で怒られます。

```typescript:app.component.ts
import { Component, OnInit } from '@angular/core';
import { UserListService } from '../user-list.service';

@Component({
  selector: 'app-root',
  template: `<user-list [users]="users$ | async"></user-list>`,
})
export class AppComponent implements OnInit {
  readonly users$: Observavle<User[]>;

  constructor(private userListService: UserListService) {
    this.users$ = this.userListService.getUsers();
  }

  ngOnInit() {
  }
}
```

```typescript:user-list.component.ts
@Component({
  selector: 'user-list',
  template: `
    <ul>
        <li *ngFor="let user of users">
    		{{ user.id }} {{ user.first_name }} {{ user.last_name }}
    	</li>
    </ul>
  `,
})
export class UserListComponent implements OnInit {
  @Input() users: User[];

  constructor() { }

  ngOnInit() {
  }
}
```

```terminal
Type 'null' is not assignable to type 'User[]'.

1 <user-list [users]="users$ | async"></user-list>
              ~~~~~
```

`User[]`型の`users`にnull入れたらアカン！！と・・・
下記の通り`UserListService`の`getUsers()`は`null`を返すことはありません。

```typescript:user-list.service.ts
@Injectable({ providedIn: 'root' })
export class UserListService {

  constructor(private http: HttpClient) { }

  getUsers(): Observable<User[]> {
    return this.http
      .get<{ data: User[] }>("https://reqres.in/api/users")
      .pipe(
        map(resp => resp.data)
      );
  }
}
```

にも関わらず`users`に`null`が入ってしまうの原因は、`AsyncPipe`の実装にあります。


https://blog.lacolaco.net/2020/02/async-pipe-initial-null-problem/

こちらの記事で詳しく解説されていますが、この記事が書かれた当時（2020年）からコードが少し変わっているため、改めて確認してみます。
（`AsyncPipe`をコピーした`MyAsyncPipe`を作り、`console.log()`をブチ込みまくりました。🤣）


`transform()`が肝となるコードです。
```typescript:async_pipe.ts
@Pipe({
  name: 'async',
  pure: false,
  standalone: true,
})
export class AsyncPipe implements OnDestroy, PipeTransform {
  private _latestValue: any = null; //<- コイツが問題
                              ~~~~ 
  private _obj: Subscribable<any>|Promise<any>|EventEmitter<any>|null = null;
  // 略

  transform<T>(obj: Observable<T>|Subscribable<T>|Promise<T>|null|undefined): T|null {
    // ①AsyncPipeに初めてObservableが渡されたとき
    if (!this._obj) {
      if (obj) {
        this._subscribe(obj);
      }
      return this._latestValue; 
    }
    // （objの参照が変わった時（別のObservableが渡された時、今回の話をする上では重要ではない））
    if (obj !== this._obj) {
      this._dispose();
      return this.transform(obj);
    }

    return this._latestValue;
  }
  // 略
}
```

`transform()`内で呼び出される重要なコードです。
```typescript
class SubscribableStrategy implements SubscriptionStrategy {
  createSubscription(async: Subscribable<any>, updateLatestValue: any): Unsubscribable {
    return async.subscribe({
      next: updateLatestValue,
      error: (e: any) => {
        throw e;
      }
    });
  }
  // 略
}

private _subscribe(obj: Subscribable<any>|Promise<any>|EventEmitter<any>): void {
  this._obj = obj;
  this._strategy = this._selectStrategy(obj);
  this._subscription = this._strategy.createSubscription(
    obj, (value: Object) => this._updateLatestValue(obj, value));
}

private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    this._ref!.markForCheck();
  }
}
```

`getUsers()`が返すObservableを🐧とすると、
ざっくりの流れとしては以下のようになります。

| No | 処理 | _obj | _latestValue |
|:-:|:-----------|:-:|:-:|
| 1 | `AsyncPipe`に🐧（=`obj`）が渡される | null | null |
| 2 | `transform()`が呼ばれる | null | null |
| 3 | `_obj===null` なので①のif文の内部へ | null | null |
| 4 | `_subscribe()`が呼ばれる | null | null |
| 5 | `this._obj`を上書き | 🐧 | null |
| 6 | `createSubscription`で🐧の購読を開始 | 🐧 | null |
| 7 | `return this._latestValue` | 🐧 | null |

今のところ、`_latestValue`は初期値の`null`のまま上書きされていませんので、`return this._latestValue`では`null`が返されることになります。これが「AsyncPipe初期値null問題」です。

# どう倒すか

動画でも記事でも示されている通り、必ず`*ngIf`や`*ngFor`といった`null`を無視するディレクティブを通すことで倒すことができます。

```typescript:app.component.ts
import { Component, OnInit } from '@angular/core';
import { UserListService } from '../user-list.service';

@Component({
  selector: 'app-root',
  template: `
    <ng-container *ngIf="users$ | async as users">
        <user-list [users]="users"></user-list>
    </ng-container>
  `,
})
export class AppComponent implements OnInit {
  readonly users$: Observavle<User[]>;

  constructor(private userListService: UserListService) {
    this.users$ = this.userListService.getUsers();
  }

  ngOnInit() {
  }
}
```

> AsyncPipeは常にNgIfやNgForによるnull安全なガードを通して利用すべきである。

というのが結論です。
また動画や下記記事では、テンプレート内で使う複数の非同期的な値を単一のストリームにまとめてスナップショット化するSingle State Streamという手法も紹介されているので、ぜひ見てみて下さい。

https://blog.lacolaco.net/2019/07/angular-single-state-stream-pattern/

---


ちなみに、動画内で
> BehaviorSubjectのような、購読と同時に値が流れることが確定しているストリームでさえもnullが入る

とおっしゃっていましたのでこちらも調べてみました。

```diff_typescript:app.component.ts
import { Component, OnInit } from '@angular/core';
import { UserListService } from '../user-list.service';

@Component({
  selector: 'app-root',
  template: `<user-list [users]="users$ | async"></user-list>`,
})
export class AppComponent implements OnInit {
  readonly users$: Observavle<User[]>;

  constructor(private userListService: UserListService) {
-   this.users$ = this.userListService.getUsers();
+   this.users$ = this.userListService.users$;
  }

  ngOnInit() {
  }
}
```

```diff_typescript:user-list.service.ts
@Injectable({ providedIn: 'root' })
export class UserListService {

+ private _usersSubject = new BehaviorSubject<User[]>([]); // <-購読と同時に[]が流れる

+ get users$() {
+   return this._usersSubject.asObservable() 
+ }

  constructor(private http: HttpClient) { }

-  getUsers(): Observable<User[]> {
-    return this.http
-      .get<{ data: User[] }>("https://reqres.in/api/users")
-      .pipe(
-        map(resp => resp.data)
-      );
-  }
+  getUsers(): void {
+    this.http
+      .get<{ data: User[] }>("https://reqres.in/api/users")
+      .pipe(map(resp => resp.data))
+      .subscribe(users => this._usersSubject.next(users));
+  }
}
```

すると、nullは返ってきません。

| No | 処理 | _obj | _latestValue |
|:-:|:-----------|:-:|:-:|
| 6 | `createSubscription`で🐧の購読を開始 | 🐧 | null |
|  | `_updateLatestValue()`が呼び出される | 🐧 | null |
|  | `this._latestValue`を上書き | 🐧 | `[]` |
|  | `markForCheck()`が呼ばれる | 🐧 | `[]` |
| 7 | `return this._latestValue` | 🐧 | `[]` |

`markForCheck()`が呼ばれると何が起きるのか分かっていないので色々と間違っているかもしれませんが、とりあえず最新の`AsyncPipe`では、`BehaviorSubject`など購読と同時に値が流れる場合は`null`が返ってこないようです。


間違いがあればご指摘いただけますと幸いです🙇‍♂️

明日は [hkt100rtkn](https://qiita.com/hkt100rtkn) さんです！