24
Help us understand the problem. What are the problem?

posted at

updated at

この辺が分かってきたあたりからRxJS使いこなせるようになってきた

前置き

以前携わっていた案件では、複数のAPIを呼んで取得したデータを基に画面の初期表示を行うという処理を大量に書いていました。

今回挙げる要点は、私がRxJSの記述でつまずいたところで、
使い分けを理解すると、かなりRxJSを使いこなせるようになったポイントです。

まだRxJSを触ったことがない入門者向きの説明ではありません。
勉強し始めた・仕事でコピペしながら書いてるけどあんま使いこなせないという方向けです。

厳密には正しい説明ではないと思いますが、
なんとなく使いこなせるようになって、気になったら正しい説明はご自身で調べてみてください。

下記の記事を読むとそもそもの理解が深まります。
◆コールバックからasync/await、promiseまでの歴史
async/await、promise・・これが最後の「JavaScriptの非同期処理完全に理解した」
◆Observableの概念
Angular6 から始める RxJS6 入門

※RxJSの記述はversion6を前提にしています。

subscribeとpipe

observable処理の結果を受けた後の処理を、pipeやsubscribeを使って記述します。
observable処理の定義を書いても、subscribeしないと実行されません。

pipeはobservable処理の結果を受けて、続けてどんな処理を行うか定義する時に使います。
pipeの中で、observableのoperatorを使って記述していきます。

pipeを書いても、やはりsubscribeしないと実行されません。

何を言ってるかよくわからないと思うので、具体的にコードを見てみましょう。

hoge() {
  this.search$().pipe(
    // search$というObservable処理でデータを取得した後にやりたいことをpipeで記述します。
    // ここでは、tapというoperatorを使って、処理を定義しています。(tapについては後述します。)
    tap(data => console.log(data))
  // データを取得して、コンソール出力まで終わった後の処理をsubscribeで記述します。
  ).subscribe(data => this.data = data);
}

// $ はobservable処理だとわかるように書いてるだけです
search$(): Observable<HogeData> {
  // ここでは記述を省略しますが、api取得系の処理は別のserviceクラスで実装している想定です
  return this.apiService.search$();
}

↓こう書いても結果は同じです

hoge() {
  this.search$().pipe(
    tap(data => console.log(data)),
    tap(data => this.data = data)
  ).subscribe();
}

fuga() {
  this.search$().pipe(
    tap(data => {
      console.log(data));
      this.data = data;
    })
  ).subscribe();
}

piyo() {
  this.search$().subscribe(data => {
      console.log(data);
      this.data = data;
  });
}

subscribeしないと、observable処理の定義をしただけで実行されません。

hoge() {
  // search$処理の後に行う処理の定義だけしておく
  // この時はまだ何の処理も行われません
  const obs$ = this.search$().pipe(
    tap(data => console.log(data)),
    tap(data => this.data = data)
  );

  // subscribeすると実行される
  obs$.subscribe();
}

[追記]tap operator内で記述すべきかsubscribe内で記述すべきか

https://rxjs-dev.firebaseapp.com/api/operators/tap
上記に記載がある通り、
tap operatorは主にデバッグや副作用の為のoperatorであり、
副作用ではない主作用に使うものではないそうです。

hoge() {
  this.search$().pipe(
    tap(data => {
      // 副作用はここで書くべき
      console.log(data);
    })
  ).subscribe(data => {
    // 主作用はここで書くべき
    this.data = data
  });
}

tapとmapの違い

tap、mapは、observableの処理後に同期的な処理を行う時に使います。
とてもよく使うoperatorです。
※上記の「tap operator内で記述すべきかsubscribe内で記述すべきか」の通りtapは副作用の処理を記述するものですが。

tapとmapの動き上の違いは、新たな値を返すか、返さないかの違いです。
ループ処理で言うと、forEachかmapの違いです。
現場でよく使うJS ES6ループ文まとめ

hoge() {
  this.search$().pipe(
    // search$というObservable処理後に、ログ出力するという同期的な処理を行う為、tapを用います。
    // ここでのログ出力結果は{id: 1, name: 'test'}です
    // 新たな値は返しません
    tap(data => console.log('tap1: ', data))
    // mapを使って新たな値を返します
    map(data => {
      return {
        id: data.id + 1,
        name: data.name + ''
      }
    }),
    // mapで処理した結果が流れてくるので、
    // ここでのログ出力結果は{id: 2, name: 'testあ'}です
    // ここはtapなので新たな値は返しません。上のmapで流れてきた値がそのまま次の処理へ渡ります
    tap(data => console.log('tap2: ', data))
  // ここでのログ出力結果は{id: 2, name: 'testあ'}です
  ).subscribe(data => console.log('subscribe: ', data))
}

search$(): Observable<HogeData> {
  const dummyData: Data = {
    id: 1,
    name: 'test'
  };
  return of(dummyData);
}

ofとfrom

observable処理でないものをobservable処理にします。
サーバー側の実装がまだできておらず、
代わりにハードコーディングでダミーデータの取得処理を書く時によく使います。

promiseの処理もobservableに変換できます。

ofとfromの違いは、ofでobservableにできないものはfromを使う。
というテキトーな理解を私はしています。

hoge() {
  this.search$().subscribe(data => console.log(data));
}

search$(): Observable<HogeData> {
  return this.apiService.search$();
}

// ↓このメソッドはapiServiceに記述している想定です
/**
 * TODO: サーバー通信処理を実装する。それまではdummyを返している
 */
search$(): Observable<HogeData> {
  const dummyData: HogeData = {
    id: 1,
    name: 'test'
  };
  // dummyDataは同期的なデータですが、ofを使ってobservableにします。
  // これで擬似的にapi通信して取得したように振る舞えます。
  return of(dummyData); 
}

mapとmergeMap(flatMap)の違い

どちらも新しい値を返します。
当時はこいつらの違いがわからず非常に苦労しましたが、簡単に言うと
オペレータ内で同期処理を行う場合はmap、observable処理を行う場合はmergeMap
というだけです。
mergeMap(flatMap)の仲間にswitchMap、concatMapがありますが、
この使い分けは今回は省きます。
※flatMapは非推奨になりました。

hoge() {
  this.search$().pipe(
    // 同期処理を行うのでmapを使う
    map(data => {
      const count = data.count;
      // 出力結果: 1
      console.log('map: ', count);
      return this.calculate(count);
    }),
    // observable処理を行うのでmergeMapを使う
    mergeMap(count => {
      // 出力結果: 2
      console.log('flatMap: ', count);
      return this.calculateObs$(count);
    })
  ).subscribe(count => {
    // 出力結果: 3
    console.log('subscribe: ', count);
  })
}

calculate(num: number): number {
  return num + 1;
}

calculateObs$(num: number): Observable<number> {
  return of(num + 1);
}

search$(): Observable<HogeData> {
  const dummyData: HogeData = {
    id: 1,
    name: 'test',
    count: 1
  };
  return of(dummyData);
}

mergeMap(flatMap)はapi1で取得したデータをキーに、
別のapi2からデータを取得
という場合に多用します。

hoge(userId: number) {
  this.searchUser$(userId).pipe(
    // 取得したuser情報をキーに、部署検索する
    // searchDepartmentはobservableメソッドなので、mapだと動きません
    mergeMap(user => {
      return this.searchDepartment$(user. departmentId);
    })
  ).subscribe(department => {
    console.log('department: ', department);
  })
}

searchUser$(userId: number): Observable<User> {
  return this.userService.fetchById(userId);
}

searchDepartment$(departmentId: number): Observable<Department> {
  return this.departmentService.fetchById(departmentId);
}

↓取得したuser情報もsubscribe内で処理したい場合の一例

hoge(userId: number) {
  this.searchUser$(userId).pipe(
    mergeMap(user => {
      return {
        // このmergeMap内で「user」はobservableではないので、
        // observable化して返します
        user: of(user),
        department: this.searchDepartment$(user. departmentId)
      }
    })
  ).subscribe(data => {
    console.log('user: ', data.user);
    console.log('department: ', data.department);
  })
}

こういう場合でもofには多々助けられました。

並列処理の救世主forkJoin

forkJoinは、observable処理を並列で行いたい場合に使います。
複数APIを呼んでから初期表示する画面では、mergeMap同等にとてもお世話になるoperatorです。

上記のmergeMapの例ではuser取得後にdepartment取得、の流れでしたが
複数のobservable処理を同時に実行したい処理はforkJoinを使います。

hoge(userId: number) {
  // user取得と会社情報取得は並列で行われます。
  // forkJoinの引数は配列で記述しないと非推奨の警告が出ます。
  forkJoin([
    this.searchUser$(userId),
    this.fetchCompanyInfo$()
  ]).subscribe(data => {
    // user取得処理と会社情報取得処理が両方実行された後、
    // subscribe内の処理が実行されます。
    console.log('user: ', data[0]);
    console.log('company: ', data[1]);
  })
}

// ↓でも結果は同じ
fuga(userId: number) {
  forkJoin([
    this.searchUser$(userId),
    this.fetchCompanyInfo$()
  ]).pipe(
    // user取得処理と会社情報取得処理が両方実行された後、
    // pipe内の処理が実行されます。
    tap(data => {
      console.log('user: ', data[0]);
      console.log('company: ', data[1]);
    })
  ).subscribe()
}

searchUser$(userId: number): Observable<User> {
  return this.userService.fetchById$(userId);
}

fetchCompanyInfo$(): Observable<Company> {
  return this.companyService.fetchCompany$();
}

forkJoinが返す型は配列のobservableです。

hoge(userId: number) {
  const obs$ = forkJoin([
    this.searchUser$(userId),
    this.fetchCompanyInfo$()
  ]);

  obs$.subscribe([user, company] => {
    console.log('user: ', user);
    console.log('company: ', company);
  })
}

// forkJoin内の順番変えるとこうなります
fuga(userId: number) {
  const obs$ = forkJoin([
    this.fetchCompanyInfo$(),
    this.searchUser$(userId)
  ]);

  obs$.subscribe([company, user] => {
    console.log('user: ', user);
    console.log('company: ', company);
  })
}

関連記事

初期表示処理でいっぱいapi呼んでformを作成しないといけない時のRxJSの書き方(最適化版)

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
24
Help us understand the problem. What are the problem?