JavaScript | 1 日 120 分 × 7 日アプリ学習:API通信アプリ(Datamuse API)

APP JavaScript
スポンサーリンク

3日目のゴールと今日やること

3日目のテーマは
「Datamuse API アプリを“使っていて気持ちいいツール”に近づける」 ことです。

技術的な柱は、いつも通りこの 3 つです。

  • fetch
  • Promise / async-await
  • エラーハンドリング

ただし今日は、それを 「入力補助」「サジェスト」「履歴」 という“アプリらしい機能”に結びつけていきます。

今日やることのイメージはこうです。

  • 入力中にサジェスト(前方一致)を出す
  • 選んだサジェストで本検索をする
  • 検索履歴を残して、クリックで再検索できるようにする
  • ローディング表示とエラーハンドリングを、この流れにきれいに組み込む

新しい文法はほぼ出てきません。
2日目までに作った「型」をどう応用するか がテーマです。


Datamuse の「前方一致」をサジェストに使う

サジェストに使うエンドポイント

サジェスト(入力補助)に使うのは、
Datamuse の sp パラメータです。

sp=
「このパターンにマッチする単語」を返します。
* がワイルドカードです。

例:

  • sp=ap* → ap で始まる単語
  • sp=*ing → ing で終わる単語

今回は、
「入力した文字で始まる単語」 をサジェストに使います。

https://api.datamuse.com/words?sp=ap*&max=10

入力中にサジェストを出す流れを考える

やりたいことの流れ

やりたいことを、人間の言葉で書くとこうです。

  • ユーザーが入力欄に文字を打つ
  • すぐに API を叩くのではなく、少し待つ(デバウンス)
  • 一定時間入力が止まったら、sp= で Datamuse に問い合わせる
  • 返ってきた候補を「サジェスト欄」に表示する
  • サジェストをクリックしたら、その単語で本検索をする

この流れを、fetch / async-await / エラーハンドリングの型に落としていきます。


デバウンスで「打つたびに API 連打」を防ぐ

デバウンスの基本

デバウンスは、
「最後の入力から◯ミリ秒経つまで待ってから処理を実行する」
というテクニックです。

JavaScript では、こう書きます。

let suggestTimer = null;

wordInput.addEventListener("input", () => {
  const text = wordInput.value.trim();

  clearTimeout(suggestTimer);

  if (!text) {
    suggestDiv.innerHTML = "";
    return;
  }

  suggestTimer = setTimeout(() => {
    fetchSuggestions(text);
  }, 400);
});
JavaScript

ここでのポイントは 2 つです。

1つ目は、clearTimeout で前のタイマーを消していること。
これにより、「入力のたびに API を叩く」のではなく、
「入力が止まったタイミングで 1 回だけ叩く」 という動きになります。

2つ目は、入力が空になったらサジェストを消していること。
これで UI がスッキリします。


サジェスト用の fetch 関数を作る

サジェスト専用の関数

async function fetchSuggestions(text) {
  const pattern = `${text}*`;
  const url = `https://api.datamuse.com/words?sp=${encodeURIComponent(pattern)}&max=8`;

  suggestDiv.textContent = "候補を取得中…";

  try {
    const response = await fetch(url);

    if (!response.ok) {
      suggestDiv.textContent = "候補の取得に失敗しました。";
      return;
    }

    const data = await response.json();

    if (!Array.isArray(data) || data.length === 0) {
      suggestDiv.textContent = "候補はありません。";
      return;
    }

    renderSuggestions(data);

  } catch (error) {
    suggestDiv.textContent = "候補の取得中に通信エラーが発生しました。";
    console.error(error);
  }
}
JavaScript

ここでも、2日目と同じパターンを使っています。

  • fetch
  • response.ok チェック
  • JSON パース
  • Array.isArray チェック
  • length === 0 のときは「該当なし」扱い
  • catch でネットワークエラー

やっていることは同じですが、
「本検索」ではなく「サジェスト」に使っている だけです。


サジェストをクリックできる UI にする

サジェストの描画

function renderSuggestions(data) {
  let html = "<h4>候補</h4>";

  data.forEach((item, index) => {
    html += `<p class="suggest-item" data-word="${item.word}">${item.word}</p>`;
  });

  suggestDiv.innerHTML = html;

  const items = suggestDiv.querySelectorAll(".suggest-item");
  items.forEach((el) => {
    el.addEventListener("click", () => {
      const w = el.dataset.word;
      wordInput.value = w;
      suggestDiv.innerHTML = "";
      fetchWords(); // 本検索を実行
    });
  });
}
JavaScript

ここでの大事なポイントは、

  • サジェストは「ただの文字列」ではなく「クリックできる要素」にしている
  • クリックしたら入力欄に反映し、そのまま本検索を呼んでいる

というところです。

fetchWords() は、2日目で作った「本検索」の関数です。
サジェストからも、ボタンからも、同じ関数を呼ぶ ことで、
コードの重複を防いでいます。


検索履歴を追加して「再検索」を簡単にする

履歴の構造を決める

履歴には、最低限これだけあれば十分です。

  • 検索した単語
  • モード(ml / rel_trg / rel_rhy)

JavaScript では、こう持ちます。

const history = [];
const historyDiv = document.getElementById("history");
JavaScript

履歴を追加する関数

function addHistory(word, mode) {
  history.unshift({ word, mode });

  if (history.length > 10) {
    history.pop();
  }

  renderHistory();
}
JavaScript

unshift は配列の先頭に追加するメソッドです。
「新しいものが上に来る」履歴になります。


履歴を表示して、クリックで再検索できるようにする

履歴の描画

function renderHistory() {
  if (history.length === 0) {
    historyDiv.textContent = "検索履歴はまだありません。";
    return;
  }

  let html = "<h3>検索履歴</h3>";

  history.forEach((item, index) => {
    const label = getModeLabel(item.mode);
    html += `
      <p class="history-item" data-index="${index}">
        ${item.word}${label}
      </p>
    `;
  });

  historyDiv.innerHTML = html;

  const items = historyDiv.querySelectorAll(".history-item");
  items.forEach((el) => {
    el.addEventListener("click", () => {
      const index = el.dataset.index;
      const h = history[index];

      wordInput.value = h.word;
      modeSelect.value = h.mode;

      fetchWords();
    });
  });
}
JavaScript

ここでも、
「履歴から再検索するときも fetchWords を呼ぶ」
という形にしています。

これで、

  • ボタン
  • サジェスト
  • 履歴

どこから検索しても、
同じロジックが動く ことになります。


本検索側に「履歴追加」を組み込む

2日目の fetchWords に 1 行足すだけ

2日目の fetchWords の成功パートに、
履歴追加を 1 行足します。

statusDiv.textContent = `${modeLabel}の取得に成功しました。`;
renderWords(data, modeLabel);
addHistory(word, mode);
JavaScript

これで、
検索が成功したときだけ履歴に残る
という自然な動きになります。


ローディング表示とサジェスト・履歴の関係

本検索とサジェストはローディングの扱いを分ける

本検索は「メインの処理」なので、
ローディング表示とボタン無効化を行います。

サジェストは「補助的な処理」なので、
ボタンは無効化せず、サジェスト欄だけに
「候補を取得中…」と出す程度に留めています。

このように、
「どの処理にどれくらいのローディングを付けるか」
を考えるのも、アプリ設計の一部です。


通信失敗時の分岐を“ユーザー目線”で整理する

本検索の失敗パターン

本検索(fetchWords)では、
次のように分岐しています。

  • 入力が空 → 「単語を入力してください」
  • HTTP エラー → 「サーバーエラーが発生しました。(ステータス)」
  • データ形式がおかしい → 「予期しない形式のデータが返されました」
  • 件数 0 → 「◯◯が見つかりませんでした」
  • ネットワークエラー → 「通信に失敗しました。ネットワークを確認してください」

これらはすべて、
「ユーザーが状況をイメージできる日本語」
になっています。

サジェストの失敗パターン

サジェスト(fetchSuggestions)では、
少し軽めのメッセージにしています。

  • HTTP エラー → 「候補の取得に失敗しました」
  • 件数 0 → 「候補はありません」
  • ネットワークエラー → 「候補の取得中に通信エラーが発生しました」

サジェストは「なくても致命的ではない」機能なので、
本検索よりも軽い扱いにしているわけです。


3日目のまとめ

3日目でやったことを、言葉で整理してみます。

Datamuse の sp パラメータを使って、
入力中にサジェストを出す機能 を作った。

サジェストはデバウンスを使って、
「入力が止まったタイミングでだけ API を叩く」ようにした。

サジェストをクリックすると、
入力欄に反映して、そのまま本検索(fetchWords)を呼ぶようにした。

検索履歴を配列で管理し、
成功した検索だけを履歴に追加するようにした。

履歴をクリックすると、
単語とモードを復元して再検索できるようにした。

本検索・サジェスト・履歴のどこからでも、
同じ fetchWords が呼ばれる構造 にした。

エラーハンドリングは、

  • 本検索 → しっかりめのメッセージ
  • サジェスト → 軽めのメッセージ

というように、
「機能の重要度に応じて出し分けた」。


今日いちばん深く理解してほしいこと

3日目の本質は、

「同じ fetch / async/await / try-catch の型を、どれだけいろんな機能に応用できるか」

というところにあります。

サジェスト
履歴
再検索
モード切り替え

これらは全部、
2日目までに作った“型”の上に乗っているだけ です。

「新しい API を覚える」のではなく、
「同じ型をどう再利用するか」 を考え始めている時点で、
もう初心者の段階は抜けつつあります。

4日目では、この Datamuse アプリに
「お気に入り」「ローカルストレージ保存」「状態管理」などを足して、
さらに“アプリらしさ”を育てていきます。

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