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

APP JavaScript
スポンサーリンク

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

5日目のテーマは
「fetch・async/await・エラーハンドリングの“型”をそのままに、ニュースアプリを“日常的に使えるレベル”へ引き上げる」
ことです。

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

  • キーワード検索
  • 期間・並び順・ソース絞り込み
  • ページネーション
  • お気に入り登録
  • ローディング表示

といった“ニュースアプリの基礎”を習得しました。

5日目はここに、

  • 検索履歴の保存
  • 最近見た記事の記録
  • 状態管理の整理
  • エラーハンドリングの再利用性アップ

を加えて、「毎日使えるニュースビューア」に近づけていきます。


検索履歴を実装する

なぜ履歴が必要なのか

ニュースアプリを使っていると、
「昨日検索したキーワードをもう一度見たい」
「よく検索するワードをすぐ呼び出したい」
という場面が必ず出てきます。

履歴は、ユーザーの操作回数を減らし、
アプリの“使いやすさ”を大きく向上させます。

履歴を配列で管理する

まずはメモリ上で履歴を管理します。

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

検索成功時に履歴へ追加します。

function addHistory(keyword) {
  if (history.includes(keyword)) {
    return;
  }
  history.unshift(keyword);
  if (history.length > 5) {
    history.pop();
  }
  renderHistory();
}
JavaScript

ここでのポイントは、

  • 重複を避ける
  • 新しいものを先頭に入れる
  • 最大件数を決めておく

という“履歴の基本ルール”を守ることです。

履歴を画面に表示する

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

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

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

  historyDiv.innerHTML = html;

  const buttons = historyDiv.querySelectorAll(".history-item");
  buttons.forEach((btn) => {
    btn.addEventListener("click", () => {
      const keyword = btn.dataset.keyword;
      keywordInput.value = keyword;
      currentKeyword = keyword;
      currentPage = 1;
      fetchNewsWithCurrentState();
    });
  });
}
JavaScript

履歴ボタンを押すと、
そのキーワードで再検索できるようになります。


最近見た記事を記録する

「お気に入り」と「最近見た」は別物

お気に入りは「保存したい記事」。
最近見た記事は「直近で開いた記事」。

この 2 つは目的が違うため、
別の状態として管理します。

const recentArticles = [];
const recentDiv = document.getElementById("recent");
JavaScript

記事を開いたときに記録する

記事リンクを押した瞬間に記録します。

function addRecent(article) {
  const exists = recentArticles.some((a) => a.url === article.url);
  if (!exists) {
    recentArticles.unshift(article);
    if (recentArticles.length > 5) {
      recentArticles.pop();
    }
  }
  renderRecent();
}
JavaScript

記事一覧に「閲覧記録」を仕込む

renderArticles のリンク部分を少し変更します。

<p>
  <a href="${url}" target="_blank" rel="noopener noreferrer" class="article-link" data-index="${index}">
    記事を読む
  </a>
</p>

リンククリック時に記録します。

const links = resultDiv.querySelectorAll(".article-link");
links.forEach((link, index) => {
  link.addEventListener("click", () => {
    addRecent(articles[index]);
  });
});
JavaScript

最近見た記事を表示する

function renderRecent() {
  if (recentArticles.length === 0) {
    recentDiv.textContent = "最近見た記事はありません。";
    return;
  }

  let html = "<h3>最近見た記事</h3>";

  recentArticles.forEach((article) => {
    html += `
      <div class="recent-item">
        <p>${article.title}</p>
        <a href="${article.url}" target="_blank" rel="noopener noreferrer">もう一度読む</a>
      </div>
    `;
  });

  recentDiv.innerHTML = html;
}
JavaScript

状態管理を整理して「迷子にならないコード」にする

状態を一箇所にまとめる

状態が増えてきたので、
オブジェクトにまとめると見通しが良くなります。

const state = {
  keyword: "",
  from: "",
  to: "",
  sortBy: "publishedAt",
  source: "",
  page: 1,
  pageSize: 10,
  isLoading: false
};
JavaScript

状態を更新する関数を作る

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

fetchNewsWithCurrentState を状態ベースに書き換える

async function fetchNewsWithCurrentState() {
  if (state.isLoading) return;

  startLoading();
  showLoadingStatus();
  renderLoadingSkeleton();

  try {
    const url = buildUrl(state);
    const response = await fetch(url);

    if (!response.ok) {
      showError(`サーバーエラー(${response.status})`);
      return;
    }

    const data = await response.json();

    if (data.status === "error") {
      showError(`エラー:${data.message}`);
      return;
    }

    showStatus(`ニュース取得成功(ページ ${state.page})`);
    renderArticles(data);
    updatePaginationInfo(data);
    addHistory(state.keyword);

  } catch (error) {
    showError(`ページ ${state.page} の取得に失敗しました。`);
    console.error(error);

  } finally {
    endLoading();
  }
}
JavaScript

状態をまとめることで、
コードの読みやすさが一気に上がります。


エラーハンドリングを「再利用できる形」にする

エラー表示を一箇所に集約する

function handleError(error, context = "") {
  if (context) {
    showError(`${context}${error.message || error}`);
  } else {
    showError(error.message || error);
  }
}
JavaScript

fetchNewsWithCurrentState の catch を簡潔にする

} catch (error) {
  handleError(error, `ページ ${state.page} の取得に失敗`);
}
JavaScript

これで、
どんなエラーでも同じ形式で表示できます。


5日目のまとめ

今日やったことを整理すると、こうなります。

  • fetch・async/await・エラーハンドリングの「型」はそのまま
  • 検索履歴を追加して、再検索を簡単に
  • 最近見た記事を記録して、読み返しやすく
  • 状態管理をオブジェクトにまとめて、コードを整理
  • エラーハンドリングを関数化して再利用性アップ

どれも「新しい文法」ではなく、
“状態管理 × DOM 更新 × 既存の fetch ロジック”
の組み合わせです。


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

5日目の本質は、

「API 通信の型はもう完成している。
これからは“状態”をどう扱うかでアプリの質が決まる」

ということです。

履歴
最近見た記事
お気に入り
ページ番号
検索条件

これらはすべて「状態」です。

状態を整理し、
UI に反映するロジックを分離すると、
アプリは驚くほど読みやすく、拡張しやすくなります。

次の 6 日目では、
このニュースアプリに「複数キーワード検索」や
「検索条件のプリセット」などを追加して、
さらに“プロダクト寄り”の仕上がりにしていきます。

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