【JavaScript】ローディング表示を実装する(非同期処理中のUI)

javascript-icon JavaScript

こんにちは、かつコーチです。

前回は「非同期処理のエラーハンドリング(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の使い方

タイトルとURLをコピーしました