なになれ

IT系のことを記録していきます。

AngularFireを使うと遭遇するRxJSでのハマりどころ

Angularを使うと漏れなくRxJSを使うことになります。
非同期処理を命令的に書くことがなくなるメリットがある反面、
宣言的プログラミングに慣れていない人にとってはRxJSを理解することが難しいです。
AngularFireを使ってWebアプリを作成する中で、遭遇したハマりどころを紹介します。
この記事では、RxJSの基本的なことは説明しません。

意図せずに処理が呼ばれる

AngularFireを使った場合にストリームがどういったタイミングで流れるかを意識しないと意図しないタイミングで処理が実行されてしまいます。
例えば、DBからデータを取得して、そのデータを条件にしてデータを削除する場合などです。

悪い例

// getUserはユーザの情報を取得して、レスポンスをvalueChangesメソッドによるObservableで返す関数です
// deleteItemは項目を削除する疑似関数です

getUser(id).subscribe((user) => {
    deleteItem(user.itemId);
});

getUser(id: string): Observable<{}> {
    return this.db.doc(`users/${id}`).valueChanges();
}

この場合、ユーザの情報を常に変更検知しているため、ユーザを作成したタイミングで、またこの処理が実行されてしまう可能性があります。

良い例

getUser(id).take(1).subscribe((user) => {
    deleteItem(user.itemId);
});

takeで処理を実行する回数を1回と指定することで、予期しない処理の実行を防止することができます。

コードのネストが深くなる

Observableによる連続的な非同期処理をsubscribeで処理すると非同期処理ごとにネストが深くなってしまいます。

悪い例

// getUserはユーザの情報、getUserDetailはユーザの詳細情報を取得し、レスポンスをObservableで返す疑似関数です。

getUser(id).subscribe(user => {
    getUserDetail(user.id).subscribe(detail => {
        // 処理内容
    });
});

良い例

getUser(id)
    .mergeMap(user => getUserDetail(user.id))
    .subscribe(detail => {
        // 処理内容
    });

mergeMapを使用することで元のストリームから異なるストリームに切り替えることができます。
イメージ的にはPromiseのthenを使って非同期処理を変えるのに似ていると思います。
このようにストリームを切り替えるオペレータにはmergeMapの他にconcatMap、switchMap、exhaustMapなどがあります。

mergeMapでデータが存在しない場合の条件分岐

mergeMapではデータが存在しない場合でもObservableを返す必要があります。

問題

// getUserはユーザの情報、getUserDetailはユーザの詳細情報を取得し、レスポンスをObservableで返す疑似関数です。

getUser(id)
    .mergeMap(user => {
        // userが存在しなかった場合にuser.idでundefinedで落ちる
        return getUserDetail(user.id);
    });

解決策

getUser(id)
    .mergeMap(user => {
        if(!user) {
            return Observable.empty();
        }
        return getUserDetail(user.id);
    });

emptyで何も値を持たないObservableを返します。

DBから取得したリストそれぞれの項目にデータを付与したい

リスト内の各データを条件に他のデータを参照して、リスト内に参照したデータを追記したいというケースがあります。
例えば、ユーザの一覧とユーザの詳細を結合して、一つの一覧で表示したい場合です。

解決策

// getUsersはユーザの一覧、getUserDetailはユーザの詳細情報を取得し、レスポンスをObservableで返す疑似関数です。

getUsers().map(users => {
    return users.map(user => {
        return getUserDetail(user.id).map(detail => {
            return {...user, ...detail};
        });
    });
}).mergeMap(users => {
    return Observable.combineLatest(users);
});

mapをネストさせることで、ユーザの一覧の各データに対して、ユーザの詳細情報を追記しています。
mapのネストによってObservableの中にObservableがある状態になりますが、最終的にcombineLatestでObservableに変えてます。
combineLatestはストリーム内の値を結合することができるので、結果的に中身のObservableだけを取り出しています。

おわりに

最後の例はこれで問題ないのか結構怪しいです。
もっと良い方法があれば知りたいです。