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;
}
JavaScriptfetchHolidays ではこう使います。
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」「設計の整理」
などを足して、さらに中級者らしい一歩を踏み込んでいきます。


