この記事は 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
このように、next
とcomplete
は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
というメソッドがあります。
add
はfinalize
と同様に、コールバック関数を引数に渡して使用します。
その他に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
を使用した場合と同様の出力順となりました。
それでは、finalize
とadd
のどちらも使用した場合の出力結果がどのようになるのか見てみましょう。
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
finalize
とadd
のコールバック関数はどちらも呼び出され、呼び出される順番はfinalize
が先でadd
が後になることが分かりました。
終了処理を実装する
finalize
とadd
の挙動を確認してみましたが、どちらも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
が活用できそうです。
おわりに
今回紹介したサンプルコードは下記のサンプルで動かすことができるので、よければ試してみてください。