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

APP JavaScript
スポンサーリンク

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

4日目のテーマは
「fetch・async/await・エラーハンドリングの“型”はそのままに、Datamuse アプリを“学習ツール”として育てる」
ことです。

ここまでであなたはすでに、

  • モード切り替え(類義語 / 連想語 / 韻)
  • お気に入り単語
  • 入力補完(前方一致サジェスト)
  • 共通の requestDatamuse 関数

といった「API 通信アプリの中核」を手に入れました。

4日目はここに、

  • 検索履歴
  • 最近見た単語
  • ローディング表示の整理
  • エラーハンドリングの“パターン化”

を足して、「語彙を増やすために毎日使えるツール」に近づけていきます。


今日のアプリのイメージを先に描く

どんな機能を足すか

4日目で目指すアプリは、こんな動きをします。

  • 単語+モード(類義語 / 連想語 / 韻)で検索
  • 結果はスコア順に表示
  • 単語をお気に入りに追加できる
  • 検索履歴が残り、クリックで再検索できる
  • 最近検索した単語一覧が表示される
  • ローディング中は状態が一目でわかる
  • 通信失敗時のメッセージが「パターン」として整理されている

fetch・async/await・エラーハンドリングの“型”は変えません。
変えるのは「状態」と「UI の振る舞い」です。


状態を「ひとまとめ」にして迷子を防ぐ

state オブジェクトを導入する

状態が増えてきたので、
バラバラの変数ではなく、オブジェクトにまとめます。

const state = {
  word: "",
  mode: "ml",
  isLoading: false,
  favorites: [],
  history: [],
  recent: []
};
JavaScript

ここに「アプリが覚えておくべきこと」を全部入れていきます。

状態を更新する小さな関数

function updateState(updates) {
  Object.assign(state, updates);
}
JavaScript

例えば、検索開始時はこう書けます。

updateState({
  word,
  mode,
  isLoading: true
});
JavaScript

「状態を一箇所に集める」ことで、
“今アプリがどういう状況なのか” が追いやすくなります。


検索履歴を実装する

履歴のルールを決める

履歴は、こういうルールにします。

  • 単語+モードの組み合わせで 1 件
  • 新しいものが先頭
  • 重複は上書き(前にあったものは削除して先頭に追加)
  • 最大 10 件まで

履歴を追加する関数

function addHistory(word, mode) {
  const key = `${word}::${mode}`;

  state.history = state.history.filter((item) => item.key !== key);

  state.history.unshift({ key, word, mode });

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

  renderHistory();
}
JavaScript

ここでのポイントは、

  • 「単語だけ」ではなく「モードも含めて履歴にする」
  • 同じ条件は 1 件だけにする

という「履歴の一意性」です。

履歴の表示

const historyDiv = document.getElementById("history");

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

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

  state.history.forEach((item) => {
    const label = getModeShortLabel(item.mode);
    html += `
      <button class="history-item" data-word="${item.word}" data-mode="${item.mode}">
        ${item.word}${label}
      </button>
    `;
  });

  historyDiv.innerHTML = html;

  const buttons = historyDiv.querySelectorAll(".history-item");
  buttons.forEach((btn) => {
    btn.addEventListener("click", () => {
      const word = btn.dataset.word;
      const mode = btn.dataset.mode;

      wordInput.value = word;
      setModeRadio(mode);

      updateState({ word, mode });
      fetchWordsForMode(word, mode);
    });
  });
}
JavaScript

setModeRadio はラジオボタンを切り替える小さな関数です。

function setModeRadio(mode) {
  const radios = document.querySelectorAll('input[name="mode"]');
  radios.forEach((r) => {
    r.checked = r.value === mode;
  });
}
JavaScript

最近検索した単語を記録する

「履歴」と「最近」はどう違う?

履歴は「条件(単語+モード)」の記録。
最近は「単語だけ」の記録として扱います。

例えば、

  • happy(類義語)
  • happy(韻)

は履歴では 2 件ですが、
最近では「happy」として 1 件でいい、という考え方もできます。

ここではシンプルに「単語だけのリスト」として持ちます。

最近リストの更新

const recentDiv = document.getElementById("recent");

function addRecent(word) {
  state.recent = state.recent.filter((w) => w !== word);
  state.recent.unshift(word);
  if (state.recent.length > 5) {
    state.recent.pop();
  }
  renderRecent();
}

function renderRecent() {
  if (state.recent.length === 0) {
    recentDiv.textContent = "最近検索した単語はありません。";
    return;
  }

  let html = "<h3>最近検索した単語</h3>";

  state.recent.forEach((word) => {
    html += `<button class="recent-item" data-word="${word}">${word}</button>`;
  });

  recentDiv.innerHTML = html;

  const buttons = recentDiv.querySelectorAll(".recent-item");
  buttons.forEach((btn) => {
    btn.addEventListener("click", () => {
      const word = btn.dataset.word;
      wordInput.value = word;
      updateState({ word });
    });
  });
}
JavaScript

検索成功時に、
addHistory(word, mode)addRecent(word) を呼びます。


ローディング表示を「パターン」として整理する

ステータス表示を一元管理する

ステータス表示を関数にまとめます。

function setStatus(message, type) {
  statusDiv.textContent = message;
  statusDiv.className = "status " + type;
}

function showLoading(message) {
  setStatus(message || "取得中です…", "loading");
}

function showSuccess(message) {
  setStatus(message, "success");
}

function showError(message) {
  setStatus(message, "error");
}
JavaScript

CSS 側で色を変えます。

.status.loading { color: #555; }
.status.success { color: #0a0; }
.status.error { color: #c00; }

ローディング開始・終了を整理する

function startLoading(message) {
  updateState({ isLoading: true });
  showLoading(message || "取得中です…");
  searchButton.disabled = true;
}

function endLoading() {
  updateState({ isLoading: false });
  searchButton.disabled = false;
}
JavaScript

fetchWordsForMode はこうなります。

async function fetchWordsForMode(word, mode) {
  if (state.isLoading) return;

  startLoading("単語を取得中です…");
  resultDiv.textContent = "";

  try {
    const params = buildSearchParams(word, mode);
    const data = await requestDatamuse(params);

    if (data.length === 0) {
      const label = getModeShortLabel(mode);
      showError(`${label}が見つかりませんでした。`);
      resultDiv.textContent = "";
      return;
    }

    const sorted = [...data].sort((a, b) => (b.score || 0) - (a.score || 0));

    const label = getModeShortLabel(mode);
    showSuccess(`${label}の取得に成功しました。`);

    renderWords(sorted, mode);
    addHistory(word, mode);
    addRecent(word);

  } catch (error) {
    const label = getModeShortLabel(mode);
    showError(`${label}の取得中にエラーが発生しました:${error.message}`);
    console.error(error);

  } finally {
    endLoading();
  }
}
JavaScript

ここでの深掘りポイントは、
「ローディング・成功・エラーの見せ方を“関数”として固定する」
ことです。

これをやっておくと、
どんな API アプリでも「見た目のルール」が揃います。


エラーハンドリングを「パターン」としてまとめる

エラーの種類を意識する

Datamuse の場合でも、エラーはざっくりこう分けられます。

  • ネットワークエラー(fetch 自体が失敗)
  • HTTP エラー(response.ok が false)
  • データ形式エラー(配列じゃないなど)

requestDatamuse の中で、
HTTP エラーと形式エラーはすでに throw しています。

async function requestDatamuse(params) {
  const baseUrl = "https://api.datamuse.com/words";
  const url = `${baseUrl}?${params.toString()}`;

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTPエラー(${response.status})`);
  }

  const data = await response.json();

  if (!Array.isArray(data)) {
    throw new Error("予期しないレスポンス形式です。");
  }

  return data;
}
JavaScript

これにより、
fetchWordsForMode 側では「まとめて catch」できます。

} catch (error) {
  const label = getModeShortLabel(mode);
  showError(`${label}の取得中にエラーが発生しました:${error.message}`);
}
JavaScript

サジェスト側のエラーは「静かに処理」

サジェストは「補助機能」なので、
失敗しても画面を赤くする必要はありません。

async function fetchSuggestions(prefix) {
  if (!prefix) {
    suggestDiv.textContent = "";
    return;
  }

  const params = new URLSearchParams();
  params.set("sp", prefix + "*");
  params.set("max", "5");

  try {
    const data = await requestDatamuse(params);

    if (data.length === 0) {
      suggestDiv.textContent = "";
      return;
    }

    renderSuggestions(data);

  } catch (error) {
    console.error("サジェスト取得中にエラー", error);
    suggestDiv.textContent = "";
  }
}
JavaScript

ここでのポイントは、
「全部のエラーを同じ重さで扱わない」
ということです。


4日目の全体像(重要部分のコードイメージ)

細部はあなたの 1〜3 日目のコードに合わせて調整してほしいですが、
構造としてはこうなります。

  • state に word / mode / isLoading / favorites / history / recent
  • requestDatamuse で fetch・async/await・エラーハンドリングを共通化
  • fetchWordsForMode で検索ロジック+履歴・最近更新
  • addHistory / renderHistory で履歴機能
  • addRecent / renderRecent で最近機能
  • setStatus / showLoading / showSuccess / showError で表示パターン統一

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

4日目の本質は、

「fetch・async/await・エラーハンドリングの“型”はもう完成している。
これからの差は“状態をどう持ち、どう見せるか”で決まる」

ということです。

単語
モード
お気に入り
履歴
最近
ローディング状態

これらは全部「状態」です。

状態を整理して、
UI に反映する関数を分けて、
エラーメッセージのパターンを決める。

それができるようになると、
どんな API でも「自分のツール」に変えていけます。

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