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

APP JavaScript
スポンサーリンク

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

5日目のテーマは
「Datamuse アプリを“毎日使える学習ツール”に近づけるために、状態を保存して、使い勝手を一段上げる」
ことです。

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

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

そこに今日は、次の要素を足します。

  • ローカルストレージで「お気に入り」「履歴」を保存する
  • アプリ起動時に保存データを読み込む
  • fetch・async/await・エラーハンドリングの“最終フォーム”を意識する
  • 「失敗したときにどう振る舞うか」を、保存機能込みで考える

「ページを閉じても、昨日の自分の学習が残っている」
そんな状態を目指します。


まず「今のアプリの構造」をざっくり整理する

4日目まででできていること

ここまでの Datamuse アプリは、だいたいこんな構造になっているはずです。

  • state に word / mode / isLoading / favorites / history / recent
  • requestDatamuse で Datamuse への fetch を共通化
  • fetchWordsForMode で検索ロジック(モード別・スコア順・エラー処理)
  • addHistory / renderHistory で検索履歴
  • addRecent / renderRecent で最近検索した単語
  • addFavorite / renderFavorites でお気に入り単語
  • fetchSuggestions / renderSuggestions で入力補完

今日はここに「保存」と「復元」を足します。


ローカルストレージの基本をおさらいする

文字列しか保存できない、というルール

localStorage は「キーと文字列」を保存する場所です。

localStorage.setItem("key", "value");
const value = localStorage.getItem("key");
JavaScript

配列やオブジェクトを保存したいときは、
JSON に変換してから保存します。

const arr = ["apple", "banana"];
localStorage.setItem("fruits", JSON.stringify(arr));

const loaded = JSON.parse(localStorage.getItem("fruits") || "[]");
JavaScript

ここでのポイントは、

  • 保存時は JSON.stringify
  • 読み込み時は JSON.parse
  • データがないときに備えて "[]""{}" をデフォルトにする

という「お約束パターン」です。


お気に入り単語をローカルストレージに保存する

state と localStorage をつなぐ

まず、state に favorites がある前提で進めます。

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

お気に入りを追加する関数を、
「保存込み」の形にします。

function addFavorite(word) {
  if (state.favorites.includes(word)) {
    showSuccess("すでにお気に入りに追加されています。");
    return;
  }

  state.favorites.push(word);
  saveFavorites();
  showSuccess(`「${word}」をお気に入りに追加しました。`);
  renderFavorites();
}
JavaScript

保存用の関数を分ける

function saveFavorites() {
  localStorage.setItem("dm_favorites", JSON.stringify(state.favorites));
}
JavaScript

起動時に読み込む

function loadFavorites() {
  const saved = localStorage.getItem("dm_favorites");
  if (!saved) {
    state.favorites = [];
    return;
  }

  try {
    const parsed = JSON.parse(saved);
    if (Array.isArray(parsed)) {
      state.favorites = parsed;
    } else {
      state.favorites = [];
    }
  } catch (e) {
    console.error("お気に入りの読み込みに失敗しました", e);
    state.favorites = [];
  }

  renderFavorites();
}
JavaScript

アプリ起動時に一度だけ呼びます。

loadFavorites();
JavaScript

深掘りポイント

ここで大事なのは、

  • 「お気に入りを追加する」と「保存する」を分けている
  • 読み込み時に try-catch を使って「壊れたデータ」に備えている

というところです。

「外部(localStorage)から来るデータは信用しすぎない」
という感覚は、API のレスポンスを扱うときとも共通しています。


検索履歴も保存する

履歴の保存・読み込み

履歴は state.history
{ key, word, mode } の配列として入っている想定で進めます。

保存

function saveHistory() {
  localStorage.setItem("dm_history", JSON.stringify(state.history));
}
JavaScript

読み込み

function loadHistory() {
  const saved = localStorage.getItem("dm_history");
  if (!saved) {
    state.history = [];
    return;
  }

  try {
    const parsed = JSON.parse(saved);
    if (Array.isArray(parsed)) {
      state.history = parsed;
    } else {
      state.history = [];
    }
  } catch (e) {
    console.error("履歴の読み込みに失敗しました", e);
    state.history = [];
  }

  renderHistory();
}
JavaScript

追加時に保存を呼ぶ

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();
  }

  saveHistory();
  renderHistory();
}
JavaScript

深掘りポイント

履歴は「ユーザーの行動の記録」です。
これを保存することで、

  • 昨日どんな単語を調べたか
  • どのモードをよく使うか

が一目でわかるようになります。

技術的には単なる JSON の保存ですが、
「状態を時間の向こう側に持ち越す」 という意味で、
アプリの“重み”が一段上がります。


最近検索した単語も保存する(シンプル版)

recent の保存・読み込み

最近検索した単語は、
単純に文字列の配列として扱っている想定です。

保存

function saveRecent() {
  localStorage.setItem("dm_recent", JSON.stringify(state.recent));
}
JavaScript

読み込み

function loadRecent() {
  const saved = localStorage.getItem("dm_recent");
  if (!saved) {
    state.recent = [];
    return;
  }

  try {
    const parsed = JSON.parse(saved);
    if (Array.isArray(parsed)) {
      state.recent = parsed;
    } else {
      state.recent = [];
    }
  } catch (e) {
    console.error("最近検索の読み込みに失敗しました", e);
    state.recent = [];
  }

  renderRecent();
}
JavaScript

追加時に保存

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

アプリ起動時の「初期化フロー」を整える

init 関数を作る

バラバラに呼んでいた初期化処理を、
ひとつの関数にまとめます。

function init() {
  loadFavorites();
  loadHistory();
  loadRecent();

  showSuccess("Datamuse 単語ツールを開始しました。");
}

init();
JavaScript

ここでのポイントは、

  • 「起動時に何が行われるか」が一目でわかる
  • 追加の初期化処理(例えばテーマ設定など)もここに足せる

という「入口の見通しの良さ」です。


fetch・async/await・エラーハンドリングの“最終フォーム”を意識する

Datamuse 用の共通関数を再確認する

5日目の時点で、
requestDatamuse はこうなっているのが理想です。

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

  let response;

  try {
    response = await fetch(url);
  } catch (networkError) {
    throw new Error("ネットワークエラーが発生しました。接続を確認してください。");
  }

  if (!response.ok) {
    throw new Error(`HTTPエラー(${response.status})が発生しました。`);
  }

  let data;

  try {
    data = await response.json();
  } catch (parseError) {
    throw new Error("レスポンスの解析に失敗しました。");
  }

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

  return data;
}
JavaScript

ここでの深掘りポイントは、

  • fetch 自体の失敗(ネットワーク)
  • HTTP ステータスエラー
  • JSON パースエラー
  • データ形式エラー

を、全部「メッセージ付きの Error」として上に投げていることです。

これにより、
上位の関数(検索・サジェストなど)は、
「何をしていて失敗したか」だけを足してユーザーに伝えればよくなります。


保存機能込みのエラーハンドリングを考える

検索側のエラー表示

検索関数は、
requestDatamuse から投げられたエラーを受け取って、
「文脈」を足して表示します。

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

ここでのポイントは、

  • 保存処理(履歴・最近)は「成功したときだけ」行う
  • エラーが起きても、localStorage のデータは壊さない
  • エラーのメッセージは「何をしようとしていたか」を含める

という「安全な振る舞い」です。


5日目の全体像をイメージでまとめる

コード全体は長くなるので、構造だけ整理するとこうです。

  • state
    • word / mode / isLoading / favorites / history / recent
  • 保存・読み込み
    • saveFavorites / loadFavorites
    • saveHistory / loadHistory
    • saveRecent / loadRecent
  • 共通 API 関数
    • requestDatamuse(fetch・async/await・エラーハンドリングの集約)
  • 検索ロジック
    • buildSearchParams
    • fetchWordsForMode
  • UI 更新
    • renderWords
    • renderFavorites
    • renderHistory
    • renderRecent
  • 状態表示
    • setStatus / showLoading / showSuccess / showError
  • 初期化
    • init(保存データの読み込み+初期メッセージ)

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

5日目の本質は、

「fetch・async/await・エラーハンドリングの“型”の上に、
状態の保存(localStorage)を自然に乗せられるようになる」

ことです。

API から取ってきたデータを、
その場で表示して終わりにするのではなく、

  • お気に入りとして残す
  • 履歴として残す
  • 最近として残す

という「時間をまたぐ設計」ができるようになると、
アプリは一気に“自分の道具”になります。

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