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

APP JavaScript
スポンサーリンク

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

4日目のテーマは
「Nager.Date 祝日アプリを“毎日使えるツール”に育てる」 ことです。

技術の柱は変わりません。

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

ただ、今日はそれを

  • 状態管理(state)
  • お気に入り祝日機能
  • localStorage による保存
  • ローディング表示の整理
  • エラー処理の整理

に結びつけていきます。

ここからが「ただ動くサンプル」から
「自分で育てていけるアプリ」 に変わるポイントです。


アプリに「状態」を持たせるという発想

なぜ state が必要なのか

3日目までのコードでは、

  • 今表示している祝日一覧
  • お気に入りにしたい祝日
  • ローディング中かどうか

などが、バラバラの変数で存在しているはずです。

これをひとつのオブジェクトにまとめると、
頭の中が一気に整理されます。

例えば、こんな形です。

const state = {
  isLoading: false,
  holidays: [],
  favorites: []
};
JavaScript

ここで大事なのは、

  • 「アプリが今どういう状態か」を全部ここに集める
  • UI はこの state をもとに描画する

という考え方です。


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

updateState という“窓口”を作る

state を直接書き換えてもいいのですが、
更新の仕方を統一するために、
小さなヘルパー関数を作ります。

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

これで、

updateState({ isLoading: true });
updateState({ holidays: data });
updateState({ favorites: newFavorites });
JavaScript

のように、
「何を変えたか」が一目でわかる形で書けます。


お気に入り祝日機能を設計する

何を「お気に入り」として保存するか

祝日の 1 件は、Nager.Date からこんな形で返ってきます。

  • date
  • localName
  • name
  • countryCode

お気に入りとして保存するなら、
最低限この 4 つがあれば十分です。

function addFavorite(holiday) {
  const exists = state.favorites.some(
    (fav) => fav.date === holiday.date && fav.countryCode === holiday.countryCode
  );

  if (exists) {
    return;
  }

  const newFavorites = [holiday, ...state.favorites];
  updateState({ favorites: newFavorites });
  saveFavorites();
  renderFavorites();
}
JavaScript

ここでやっていることは、

  • 同じ日付・同じ国の祝日がすでにお気に入りにあるかチェック
  • なければ先頭に追加
  • state を更新
  • localStorage に保存
  • UI を再描画

という流れです。


localStorage にお気に入りを保存する

保存は JSON に変換して行う

localStorage は文字列しか保存できないので、
配列やオブジェクトは JSON に変換します。

function saveFavorites() {
  try {
    const json = JSON.stringify(state.favorites);
    localStorage.setItem("nd_favorites", json);
  } catch (error) {
    console.error("お気に入りの保存に失敗しました", error);
  }
}
JavaScript

起動時に読み込む

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

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

  renderFavorites();
}
JavaScript

ここでのポイントは、

  • localStorage に何もない場合は空配列にする
  • JSON.parse が失敗する可能性を try / catch で受け止める
  • 配列でなければ捨てる(壊れたデータを無理に使わない)

という「慎重さ」です。


お気に入り一覧を画面に表示する

シンプルな描画関数

const favoritesDiv = document.getElementById("favorites");

function renderFavorites() {
  if (!state.favorites.length) {
    favoritesDiv.textContent = "お気に入りの祝日はまだありません。";
    return;
  }

  let html = "<h3>お気に入りの祝日</h3>";

  state.favorites.forEach((item, index) => {
    html += `<p data-index="${index}" class="favorite-item">
      ${item.date}${item.localName}${item.name} / ${item.countryCode}
    </p>`;
  });

  favoritesDiv.innerHTML = html;

  const items = favoritesDiv.querySelectorAll(".favorite-item");
  items.forEach((el) => {
    el.addEventListener("click", () => {
      const index = Number(el.dataset.index);
      const fav = state.favorites[index];
      yearInput.value = fav.date.slice(0, 4);
      countrySelect.value = fav.countryCode;
      fetchHolidays();
    });
  });
}
JavaScript

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

  • お気に入りをクリックすると、その年・その国に切り替えて再取得する
  • 「お気に入りがただの飾り」ではなく、「再検索のショートカット」になっている

というところです。


祝日一覧に「お気に入りボタン」を付ける

renderHolidays を拡張する

3日目までの renderHolidays は、
祝日をただ表示するだけでした。

ここに「お気に入りに追加」ボタンを付けます。

function renderHolidays(data) {
  updateState({ holidays: data });

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

  const countryCode = data[0].countryCode;
  const year = data[0].date.slice(0, 4);

  let html = `<h3>${year}${countryCode} の祝日一覧</h3>`;

  data.forEach((item, index) => {
    html += `
      <p>
        ${item.date}${item.localName}${item.name}
        <button data-index="${index}" class="fav-add-button">★</button>
      </p>
    `;
  });

  resultDiv.innerHTML = html;

  const buttons = resultDiv.querySelectorAll(".fav-add-button");
  buttons.forEach((btn) => {
    btn.addEventListener("click", () => {
      const index = Number(btn.dataset.index);
      const holiday = state.holidays[index];
      addFavorite(holiday);
    });
  });
}
JavaScript

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

  • state.holidays に「今表示している祝日一覧」を保存している
  • ボタンには index だけを持たせて、実データは state から取る
  • これにより、DOM に余計な情報を持たせずに済む

という設計のきれいさです。


ローディング表示を state と連動させる

isLoading をちゃんと使う

3日目までの setLoading を、
state と一緒に整理します。

function setLoading(isLoading, message) {
  updateState({ isLoading });

  if (isLoading) {
    statusDiv.textContent = message || "取得中です…";
  }

  fetchButton.disabled = isLoading;
  thisYearButton.disabled = isLoading;
  prevYearButton.disabled = isLoading;
  nextYearButton.disabled = isLoading;
}
JavaScript

fetchHolidays ではこう使います。

setLoading(true, `${year}年の祝日を取得中です…`);
resultDiv.textContent = "";

try {
  // fetch 〜 JSON 〜 分岐
} catch (error) {
  // エラー処理
} finally {
  setLoading(false);
}
JavaScript

これで、

  • 通信中はボタンが全部無効
  • 終わったら一括で元に戻る

という一貫した動きになります。


fetch / async-await / エラーハンドリングを“型”として固める

4日目版 fetchHolidays の全体像

ここが今日の中核です。

async function fetchHolidays() {
  const rawYear = yearInput.value.trim();
  const countryCode = countrySelect.value;

  const parsed = parseYear(rawYear);
  if (!parsed.ok) {
    statusDiv.textContent = parsed.message;
    resultDiv.textContent = "";
    return;
  }

  const year = parsed.value;

  setLoading(true, `${year}年の祝日を取得中です…`);
  resultDiv.textContent = "";

  try {
    const url = `https://date.nager.at/api/v3/PublicHolidays/${year}/${countryCode}`;
    const response = await fetch(url);

    if (!response.ok) {
      if (response.status === 404) {
        statusDiv.textContent = "指定された年または国の祝日が見つかりませんでした。";
      } else if (response.status >= 500) {
        statusDiv.textContent = "サーバー側でエラーが発生しています。時間をおいて再試行してください。";
      } else {
        statusDiv.textContent = `サーバーエラーが発生しました。(${response.status})`;
      }
      return;
    }

    const data = await response.json();

    if (!Array.isArray(data)) {
      statusDiv.textContent = "予期しない形式のデータが返されました。";
      console.error("Unexpected data:", data);
      return;
    }

    if (data.length === 0) {
      statusDiv.textContent = "祝日が見つかりませんでした。";
      resultDiv.textContent = "";
      return;
    }

    statusDiv.textContent = "祝日の取得に成功しました。";
    renderHolidays(data);

  } catch (error) {
    statusDiv.textContent = "通信に失敗しました。ネットワークを確認してください。";
    console.error(error);

  } finally {
    setLoading(false);
  }
}
JavaScript

ここに、
fetch / async-await / エラーハンドリングの「完成形」が入っています。


エラーハンドリングを“ユーザー目線”で見直す

どこで何を伝えているか

この関数の中では、エラーがいくつかのレイヤーに分かれています。

入力エラー
年が数字でない、範囲外などは parseYear が検知し、
「年は 1900〜2100 の範囲で入力してください。」のように伝えます。

HTTP エラー
response.ok が false のときに、

  • 404 → 「その条件の祝日がない」
  • 500 以上 → 「サーバー側の問題」
  • その他 → 一般的なサーバーエラー

として伝えています。

データ形式エラー
配列でない場合は「予期しない形式のデータ」として扱い、
コンソールに中身を出します。

該当なし
配列が空のときは「祝日が見つかりませんでした。」と伝えます。
これはエラーではなく「正常な結果」です。

ネットワークエラー
fetch 自体が失敗したときは catch に入り、
「通信に失敗しました。ネットワークを確認してください。」と伝えます。

こうやって整理してみると、
「どのエラーがどこで処理されているか」 がはっきり見えてきます。


初期化処理で「アプリらしさ」を出す

ページ読み込み時にやっておきたいこと

アプリ起動時に、次のことをやっておくと気持ちいいです。

  • 今年の年をセットする
  • お気に入りを localStorage から読み込む
function init() {
  const currentYear = new Date().getFullYear();
  yearInput.value = currentYear;
  loadFavorites();
  statusDiv.textContent = `${currentYear}年の祝日を取得する準備ができました。`;
}

document.addEventListener("DOMContentLoaded", init);
JavaScript

これで、
ページを開いた瞬間から「使える感じ」が出ます。


4日目のまとめ

今日あなたがやったことを、言葉で整理してみます。

アプリに state を導入して、

  • isLoading
  • holidays
  • favorites

をひとまとめにした。

updateState で状態更新の書き方を統一した。

祝日 1 件を「お気に入り」として扱う構造を決め、
addFavorite で重複チェック・state 更新・保存・再描画までを一気に行うようにした。

localStorage にお気に入りを保存し、
起動時に loadFavorites で読み込むようにした。

renderFavorites でお気に入り一覧を表示し、
クリックでその年・その国の祝日を再取得できるようにした。

renderHolidays に「お気に入り追加ボタン」を付け、
state.holidays と index を使って対象の祝日を特定するようにした。

setLoading でローディング状態とボタンの有効・無効を一括管理した。

fetchHolidays の中で、
入力エラー・HTTP エラー・データ形式エラー・該当なし・ネットワークエラーを
それぞれ別のメッセージで扱うようにした。

init で「今年の年をセット」「お気に入り読み込み」を行い、
起動直後から使えるアプリにした。


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

4日目の本質は、

「fetch / async-await / エラーハンドリングは、“状態管理”と組み合わさったときにアプリになる」

ということです。

祝日を取ってくる
お気に入りに入れる
保存する
再表示する
再検索する

この一連の流れの中に、
fetch と Promise と try/catch が自然に溶け込んでいる。

ここまで来ているあなたは、
もう「API を試す人」ではなく、
「API を使って自分のツールを作る人」 です。

5日目では、この祝日アプリに
「複数国の比較」「Promise.all」「設計の整理」
などを足して、さらに中級者らしい一歩を踏み込んでいきます。

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