こんにちは、かつコーチです。
前回は「非同期処理のエラーハンドリング(try-catch)」で、通信が失敗したときの対処方法を解説しました。
今回はAPI通信シリーズの締めくくりとして、「通信が終わるまでの間、ユーザーに何を見せるか」というテーマを扱います。
エラー処理と同じくらい大事なのに、意外と後回しにされがちな部分です。
しっかり実装して、使い心地の良い画面を作れるようになりましょう。
なぜローディング表示が必要なのか
「何も表示されない時間」がユーザーに与える印象
これまでの記事で書いてきたfetchの処理は、リクエストしてから結果が返ってくるまでの間、画面には何も変化がありませんでした。
通信環境が良ければ一瞬で終わりますが、回線が遅い状況やサーバーの応答が遅い状況では、数秒間「画面が固まっているように見える時間」が発生します。
ユーザーからすると、この「何も起きていないように見える時間」がとても不安に感じられます。
「ボタンを押し間違えたかな」「もう一度押した方がいいのかな」と、二重にクリックしてしまう原因にもなります。
ローディング表示が解決すること
ここに「読み込み中です」という表示を挟むだけで、ユーザーは「今、裏側で処理が進んでいるんだな」と安心して待てるようになります。
見た目の変化としては地味ですが、体感的な使いやすさに大きく影響する、非常に重要なUI要素です。
ローディング表示の基本実装
下準備:HTML構造
まずは、ローディング表示とデータ表示、それぞれの入れ物をHTMLに用意します。
<div id="loading" style="display: none;">読み込み中です...</div>
<ul id="user-list"></ul>
loading の要素は最初は非表示にしておき、通信中だけ表示を切り替える、という作り方にします。
通信の前後でローディング表示を切り替える
続いて、JavaScript側の実装です。
async function showUserList() {
const loadingElement = document.getElementById("loading");
const listElement = document.getElementById("user-list");
loadingElement.style.display = "block"; // 通信開始前に表示する
listElement.innerHTML = "";
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(`status: ${response.status}`);
}
const users = await response.json();
users.forEach((user) => {
const li = document.createElement("li");
li.textContent = user.name;
listElement.appendChild(li);
});
} catch (error) {
console.error("取得エラー:", error);
listElement.textContent = "データの取得に失敗しました。";
} finally {
loadingElement.style.display = "none"; // 成功・失敗どちらでも非表示に戻す
}
}
showUserList();
ポイントは、ローディング表示を消す処理を finally の中に書いていることです。
前回の記事でも触れた通り、finally は成功しても失敗しても必ず実行されるため、「エラーになったのにローディング表示が消えないまま残ってしまう」というミスを防げます。
つまずきやすいポイント
finallyを使わずにローディング解除を書いて消し忘れる
私が実際に経験した失敗が、まさにこの「ローディング表示の消し忘れ」でした。
❌ Before:成功パターンにだけ非表示処理を書いてしまう
async function showUserList() {
const loadingElement = document.getElementById("loading");
const listElement = document.getElementById("user-list");
loadingElement.style.display = "block";
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await response.json();
users.forEach((user) => {
const li = document.createElement("li");
li.textContent = user.name;
listElement.appendChild(li);
});
loadingElement.style.display = "none"; // 成功時にしか実行されない
} catch (error) {
console.error("取得エラー:", error);
// catchの中に非表示処理を書き忘れている
}
}
このコードでは、通信が成功した場合はローディング表示がきちんと消えますが、エラーが起きて catch に入った場合、ローディング表示を消す処理が一切実行されません。
私は開発中にわざとネットワークを切ってこのコードを試したとき、「読み込み中です」の表示がいつまでも画面に残り続けてしまい、「あれ、フリーズした?」と一瞬焦った経験があります。
原因は単純で、非表示にする処理を成功パターンのところにしか書いていなかったからでした。
✅ After:finallyに非表示処理をまとめる
async function showUserList() {
const loadingElement = document.getElementById("loading");
const listElement = document.getElementById("user-list");
loadingElement.style.display = "block";
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await response.json();
users.forEach((user) => {
const li = document.createElement("li");
li.textContent = user.name;
listElement.appendChild(li);
});
} catch (error) {
console.error("取得エラー:", error);
} finally {
loadingElement.style.display = "none"; // 成功・失敗どちらでも必ず実行される
}
}
「表示する処理」と「消す処理」がセットになる場面では、finally を使えないか一度検討してみる、というのを癖にしておくとこの手のミスを防げます。
ボタン操作と組み合わせたローディング表示
二重クリックを防ぐ
ローディング中にボタンを何度も押せてしまうと、同じ通信が重複して発生してしまいます。
<button id="load-button">ユーザー一覧を取得</button>
<div id="loading" style="display: none;">読み込み中です...</div>
<ul id="user-list"></ul>
const button = document.getElementById("load-button");
async function showUserList() {
const loadingElement = document.getElementById("loading");
const listElement = document.getElementById("user-list");
loadingElement.style.display = "block";
button.disabled = true; // 通信中はボタンを押せなくする
listElement.innerHTML = "";
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(`status: ${response.status}`);
}
const users = await response.json();
users.forEach((user) => {
const li = document.createElement("li");
li.textContent = user.name;
listElement.appendChild(li);
});
} catch (error) {
console.error("取得エラー:", error);
listElement.textContent = "データの取得に失敗しました。";
} finally {
loadingElement.style.display = "none";
button.disabled = false; // 通信が終わったら押せるように戻す
}
}
button.addEventListener("click", showUserList);
button.disabled = true でボタンを押せない状態にし、finally の中で false に戻すことで、通信中の二重クリックを防いでいます。
ローディング表示と合わせてこの一手間を加えるだけで、想定外の重複リクエストをかなり防げるようになります。
CSSアニメーションで見た目を工夫する
テキストだけの「読み込み中です」でも十分機能しますが、簡単なスピナー(回転アイコン)を加えると、より「動いている感」が伝わりやすくなります。
<style>
.spinner {
display: none;
width: 24px;
height: 24px;
border: 4px solid #ddd;
border-top: 4px solid #333;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
<div id="loading" class="spinner"></div>
const loadingElement = document.getElementById("loading");
loadingElement.style.display = "block"; // 通信開始時
// ...
loadingElement.style.display = "none"; // finallyで非表示に戻す
display の切り替え方法はこれまでと同じで、中身のHTML・CSSをスピナーに差し替えるだけで見た目を改善できます。
まずはテキスト表示で仕組みを理解してから、余裕があればこうした見た目の調整に挑戦してみてください。
まとめ
この記事のポイント
- 通信中に「何も表示されない時間」があると、ユーザーは不安を感じやすい
- ローディング表示の切り替えは、通信開始時に表示・
finallyで必ず非表示に戻す - 非表示処理を成功パターンにだけ書くと、エラー時に表示が残り続けるバグになる
- ボタンを
disabledにすることで、通信中の二重クリックも合わせて防げる
次に読むべき記事
API通信の基本が一通り身についたところで、次はブラウザにデータを保存できるLocalStorage・SessionStorageの使い方を見ていきます。
→ 次の記事:LocalStorage・SessionStorageの使い方