gapiのPromiseでAngularのChangeDetectionが走らない


概要

例えばSpreadSheetAPIを呼ぶのにgapi.clientを使うことがあるかと思います。Angularで普段通りこのPromiseを受け取ってasyncPipeなり、subscribeして変数に突っ込むなりしても画面が更新されない問題に当たりました。正しいキーワードで検索できればすぐなのですが問題がピンポイントすぎて(よもやgapiの時だけそんなことが起きると思っていなかった)時間を溶かしてしまったので、これから当たる人がすぐ気づけると良いなという気持ちでメモに残します。

Disclaimer

※頭に置くとTwitter等のサムネの引用でDisclaimerが使われてしまうのでこの位置に挟んでいます。
この記事を執筆現在私はGoogleで働いています。記事内でGoogleのプロダクトについて言及するため明確に所属を記載していますが、このことは私が記事内に登場するプロダクトについて特別詳しい知識を持っていることを意味しません。1ユーザーが書いた記事としてお楽しみください。

問題のコード

早速見ていきましょう。

service.ts
  getSheets() {
    // gapi.clientがロードされたら
    return this.oauth.ensureGapiLoaded().pipe(
      mergeMap(() => {
        // SpreadsheetAPIを呼ぶ
        return gapi.client.sheets.spreadsheets.get({
          spreadsheetId: spreadsheetId,
        });
      }),
      map((response) => {
        // 欲しい形に整形する(関係ないので省略)
      })
    );
  }

特に変わったところはない、データを取りにいくサービスのメソッドですね。
これをコンポーネント側で普段通り使います。

component.ts
this.service.getSheets().subscribe(res => {
    this.sheets = res;
    console.log(res); // 確認用
});
component.html
<div *ngFor="let sheet of sheets">{{sheet.name}}</div>

こちらも普通です。ここから期待される動きは
1. サービスから値が帰ってきて変数に代入される
2. 変更が検知されて再描画される
3. 画面にSheetの名前が見える
ですね。

ところが、実際やってみるとcomponent.tsに値が来た時点でconsole.log()で値がきちんと表示されるにもかかわらず画面は真っ白のままです。あれれ…

私は自分が超基礎的な何かをすっぽり忘れてしまったのかと思い色々試したのですが(ここで時間が溶けた)、部分的にmockのObservableと入れ替えて試したところ、gapiを置き換えた時は普通に画面が更新されたのでここでやっとgapiが原因だと気がつきました。

問題を解決するコード

一度原因に気づけばあとは検索ですぐ答えを見つけることが出来ました。
どうやらgapiはIE対応するために独自実装されたPromiseを使っているらしく、これがAngularのChangeDetectionとうまく動かないようです。
GithubのIssueを上げてくれた人、回答してくれた人に感謝を捧げつつ 、提案されているworkaroundを試します。

service.ts
  getSheets() {
    return this.oauth.ensureGapiLoaded().pipe(
      mergeMap(() => {
        // Promise.resolve()でラップする
        return Promise.resolve(gapi.client.sheets.spreadsheets.get({
          spreadsheetId: spreadsheetId,
        }));
      }),
      map((response) => {
      })
    );
  }

これで画面がきちんと更新されるようになりました 🎉
短い記事でしたがここまでお付き合いいただきありがとうございます。
いつか誰かの役に立てば幸いです。