この記事は Angular Advent Calendar 2021 の13日目の記事です。
昨日は @okkn さんの Angular初心者がIPアドレス計算練習アプリを作ってみた でした。

はじめに

AngularでWeb開発をしていると、HTTPリクエストを行い、待機中にローディングのスピナーを表示したいというケースはよくあると思います。

弊社のUIチーム内で、HTTPリクエストの終了処理の実装方法が曖昧になっていたので、調査を行ってみることにしました。

HTTPリクエストを行う

AngularではHTTPリクエストを行うために、基本的にHttpClientを使用します。

HttpClientのリクエストメソッドはObservableを返すので、そのObservableをサブスクライブすることでHTTPリクエストを行うことができます。

constructor(private http: HttpClient) { }

fetch(): void {
  this.http.get(/* URL */).subscribe(res => {
    console.log(res);
  });
}

Observableをサブスクライブする際に、next, error, completeのコールバック関数を渡すことができます。
nextはObservableに値が流れた時、errorはObservableでエラーが発生した時、completeはObservableが完了した時に呼び出されます。

これらのコールバック関数を使用してHTTPリクエストの終了処理を実装できるか調査するため、以下のコードを実行した時の出力結果を見てみます。

constructor(private http: HttpClient) { }

fetch(): void {
  console.log('start');
  this.http.get(/* URL */).subscribe({
    next: () => {
      console.log('next');
    },
    error: () => {
      console.log('error');
    },
    complete: () => {
      console.log('complete');
    },
  });
}

HTTPリクエスト成功時

start
(待機中)
next
complete

HTTPリクエスト失敗時

start
(待機中)
error

このように、nextcompleteはHTTPリクエスト成功時は呼び出されますが、失敗時は呼び出されません。
また、HTTPリクエストの待機中にアンサブスクライブした場合は、これらのコールバック関数はすべて呼び出されません。

なので、これらのコールバック関数はHTTPリクエストの終了処理の実装には向いていません。

finalizeとadd

それでは、HTTPリクエストの終了処理を実装するにあたって、候補となる2つの方法について紹介します。

finalize

Observableのpipeメソッドに引数として渡し使用するオペレーターの中に、finalizeというオペレーターがあります。
finalizeは、コールバック関数を引数に渡して使用します。

早速以下のコードを実行した時の出力結果を見てみましょう。

testFinalize(): void {
  console.log('start');
  this.apiService
    .fetch()
    .pipe(
      finalize(() => {
        console.log('finalize');
      })
    )
    .subscribe({
      next: () => {
        console.log('next');
      },
      error: () => {
        console.log('error');
      },
      complete: () => {
        console.log('complete');
      },
    });
}

HTTPリクエスト成功時

start
(待機中)
next
complete
finalize

HTTPリクエスト失敗時

start
(待機中)
error
finalize

このようにfinalizeのコールバック関数は、HTTPリクエストの成功・失敗を問わず、終了したタイミングで呼び出されます。
また、HTTPリクエストの待機中にアンサブスクライブした場合にも呼び出されます。

add

Observableをサブスクライブした戻り値であるSubscriptionに、addというメソッドがあります。
addfinalizeと同様に、コールバック関数を引数に渡して使用します。

その他にSubscriptionを渡すこともでき、その場合はコールバック関数が呼び出される代わりに、渡したSubscriptionがアンサブスクライブされます。

こちらもコードを実行した時の出力結果を見てみましょう。

testAdd(): void {
  console.log('start');
  this.apiService
    .fetch()
    .subscribe({
      next: () => {
        console.log('next');
      },
      error: () => {
        console.log('error');
      },
      complete: () => {
        console.log('complete');
      },
    })
    .add(() => {
      console.log('add');
    });
  }

HTTPリクエスト成功時

start
(待機中)
next
complete
add

HTTPリクエスト失敗時

start
(待機中)
error
add

addを使用した場合の出力結果は、finalizeを使用した場合と同様の出力順となりました。
それでは、finalizeaddのどちらも使用した場合の出力結果がどのようになるのか見てみましょう。

testFinalizeAndAdd(): void {
  console.log('start');
  this.apiService
    .fetch()
    .pipe(
      finalize(() => {
        console.log('finalize');
      })
    )
    .subscribe({
      next: () => {
        console.log('next');
      },
      error: () => {
        console.log('error');
      },
      complete: () => {
        console.log('complete');
      },
    })
    .add(() => {
      console.log('add');
    });
  }

HTTPリクエスト成功時

start
(待機中)
next
complete
finalize
add

HTTPリクエスト失敗時

start
(待機中)
error
finalize
add

finalizeaddのコールバック関数はどちらも呼び出され、呼び出される順番はfinalizeが先でaddが後になることが分かりました。

終了処理を実装する

finalizeaddの挙動を確認してみましたが、どちらもHTTPリクエストの終了処理の実装に適していそうです。

チーム内で話し合ったところ、コードの記述順が成功 => 失敗 => 終了となり直感的で読みやすいと感じたため、addを使用して終了処理を実装することになりました。

最終的なHTTPリクエストの終了処理はこのようになりました。

data?: Data;
loading = false;

constructor(private http: HttpClient) { }

fetch(): void {
  this.loading = true;

  this.http.get(/* URL */).subscribe({
    next: data => {
      this.data = data;
      alert('データの取得に成功しました。');
    },
    error: () => {
      alert('データの取得に失敗しました。');
    },
  }).add(() => {
    this.loading = false;
  });
}

Facadeなど、サブスクライブせずにObservableを返す場合にはfinalizeが活用できそうです。

おわりに

今回紹介したサンプルコードは下記のサンプルで動かすことができるので、よければ試してみてください。