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

APP JavaScript
スポンサーリンク

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

4日目のテーマは
「fetch・async/await・エラーハンドリングの“型”はそのままに、ニュースアプリの“使い勝手”を一段上げる」
ことです。

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

  • キーワード+条件付きで NewsAPI から記事を取得する
  • ページネーション(前へ / 次へ)を実装する
  • ローディング状態とボタン制御を入れる
  • 通信失敗時にページ番号を意識したエラーメッセージを出す

という「ニュース API 通信アプリの骨格」を手に入れました。

4日目はここに、

  • ソース(ニュースサイト)で絞り込む
  • 記事の「お気に入り」機能を付ける
  • ローディング表示を少しリッチにする
  • エラー表示を“パターン化”して整理する

を足して、「毎日触りたくなるニュースビューア」に近づけていきます。


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

どんな機能を足すか

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

  • キーワード・期間・並び順・ニュースソースを指定して検索
  • 1 ページ 10 件、ページネーション付き
  • 記事ごとに「★ お気に入り」ボタンがあり、クリックすると別枠に保存
  • お気に入りはページを変えても残る
  • ローディング中は「読み込み中…」+簡単なスケルトン表示
  • エラーは「入力エラー」「通信エラー」「API エラー」で見た目を揃える

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


fetch・async/await・エラーハンドリングの「型」をもう一度固定する

中心となる非同期処理のフォーム

3日目までで、中心となる非同期処理はほぼこの形になっていました。

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

  startLoading();
  showStatus("ニュースを取得中です…");
  resultDiv.textContent = "";

  try {
    const url = buildUrl({
      keyword: currentKeyword,
      from: currentFrom,
      to: currentTo,
      sortBy: currentSortBy,
      page: currentPage,
      pageSize
    });

    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(`ニュースの取得に成功しました。(ページ ${currentPage})`);
    renderArticles(data);
    updatePaginationInfo(data);

  } catch (error) {
    showError(`ページ ${currentPage} の取得に失敗しました。ネットワークを確認してください。`);
    console.error(error);

  } finally {
    endLoading();
  }
}
JavaScript

ここで大事なのは、

  • fetch の前後に「ローディング開始 / 終了」があること
  • HTTP エラー(!response.ok)と API エラー(data.status === “error”)を分けていること
  • catch では「通信レベルの失敗」を扱っていること

4日目は、この「型」を崩さずに、周辺の UI と状態を育てていきます。


ソース(ニュースサイト)で絞り込む機能を追加する

NewsAPI の source / domain をどう使うか

NewsAPI には、ニュースソースを絞り込む方法がいくつかあります。

  • sources: NewsAPI が定義しているソース ID(例: bbc-news
  • domains: ドメイン名(例: nytimes.com

中級編 4日目では、まずはシンプルに「よく使うソースをセレクトボックスで選ぶ」形にします。

UI にソース選択を追加する

HTML にセレクトボックスを追加します。

<select id="sourceSelect">
  <option value="">すべてのソース</option>
  <option value="bbc-news">BBC News</option>
  <option value="the-verge">The Verge</option>
  <option value="techcrunch">TechCrunch</option>
</select>

JavaScript で取得します。

const sourceSelect = document.getElementById("sourceSelect");
let currentSource = "";
JavaScript

buildUrl に source を組み込む

function buildUrl({ keyword, from, to, sortBy, page, pageSize, source }) {
  const params = new URLSearchParams({
    q: keyword,
    apiKey: API_KEY,
    language: "ja",
    sortBy: sortBy || "publishedAt",
    page: String(page || 1),
    pageSize: String(pageSize || 10)
  });

  if (from) params.set("from", from);
  if (to) params.set("to", to);
  if (source) params.set("sources", source);

  return `${baseUrl}?${params.toString()}`;
}
JavaScript

検索開始時に source も状態として保存する

searchButton.addEventListener("click", () => {
  const keyword = keywordInput.value.trim();
  const from = fromInput.value;
  const to = toInput.value;
  const sortBy = sortSelect.value;
  const source = sourceSelect.value;

  const error = validateKeyword(keyword);
  if (error) {
    showError(error);
    resultDiv.textContent = "";
    return;
  }

  currentKeyword = keyword;
  currentFrom = from;
  currentTo = to;
  currentSortBy = sortBy;
  currentSource = source;
  currentPage = 1;

  fetchNewsWithCurrentState();
});
JavaScript

fetchNewsWithCurrentState で source を渡す

const url = buildUrl({
  keyword: currentKeyword,
  from: currentFrom,
  to: currentTo,
  sortBy: currentSortBy,
  page: currentPage,
  pageSize,
  source: currentSource
});
JavaScript

ここでのポイントは、
「条件が増えても、fetch の書き方は変わらない」
ということです。
変わるのは URL のパラメータだけです。


記事の「お気に入り」機能を実装する

お気に入りをどう表現するか

お気に入りは、
「記事の情報を配列で持つ」のがシンプルです。

const favorites = [];
const favoritesDiv = document.getElementById("favorites");
JavaScript

1 件の記事を「お気に入り用のオブジェクト」として保存します。

function addFavorite(article) {
  const exists = favorites.some((fav) => fav.url === article.url);
  if (exists) {
    showStatus("すでにお気に入りに追加されています。");
    return;
  }

  favorites.push(article);
  showStatus("お気に入りに追加しました。");
  renderFavorites();
}
JavaScript

記事一覧に「★ お気に入り」ボタンを付ける

renderArticles を少し拡張します。

function renderArticles(data) {
  const articles = data.articles;

  if (!articles || articles.length === 0) {
    resultDiv.textContent = "該当するニュースが見つかりませんでした。";
    return;
  }

  let html = "";

  articles.forEach((article, index) => {
    const title = article.title || "タイトルなし";
    const description = article.description || "説明文はありません。";
    const url = article.url;
    const source = article.source?.name || "不明なソース";

    html += `
      <div class="article" data-index="${index}">
        <h3>${title}</h3>
        <p>${description}</p>
        <p>提供元:${source}</p>
        <p><a href="${url}" target="_blank" rel="noopener noreferrer">記事を読む</a></p>
        <button class="favorite-button">★ お気に入りに追加</button>
      </div>
    `;
  });

  resultDiv.innerHTML = html;

  const buttons = resultDiv.querySelectorAll(".favorite-button");
  buttons.forEach((btn, index) => {
    btn.addEventListener("click", () => {
      const article = articles[index];
      addFavorite(article);
    });
  });
}
JavaScript

ここでのポイントは、

  • API から返ってきた articles をそのままお気に入りに渡している
  • URL をキーとして「同じ記事を重複登録しない」ようにしている

というところです。

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

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

  let html = "<h3>お気に入り記事</h3>";

  favorites.forEach((article) => {
    const title = article.title || "タイトルなし";
    const url = article.url;
    const source = article.source?.name || "不明なソース";

    html += `
      <div class="favorite-article">
        <p>★ ${title}${source})</p>
        <p><a href="${url}" target="_blank" rel="noopener noreferrer">記事を読む</a></p>
      </div>
    `;
  });

  favoritesDiv.innerHTML = html;
}
JavaScript

お気に入りは「ページを変えても残る」ので、
「API の結果」とは別の“アプリの状態”として扱うことになります。


ローディング表示を少しリッチにする

スケルトン表示の考え方

スケルトン表示とは、
「まだ中身はないけど、ここにカードが並ぶよ」という
“薄いグレーのダミー UI”のことです。

ここでは、シンプルに「ダミー記事カード」を表示してみます。

function renderLoadingSkeleton() {
  let html = "";

  for (let i = 0; i < 3; i++) {
    html += `
      <div class="article skeleton">
        <h3>読み込み中...</h3>
        <p>説明文を読み込み中です…</p>
        <p>提供元:読み込み中…</p>
      </div>
    `;
  }

  resultDiv.innerHTML = html;
}
JavaScript

fetchNewsWithCurrentState の中で、
ローディング開始時にこれを呼びます。

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

  startLoading();
  showStatus("ニュースを取得中です…");
  renderLoadingSkeleton();

  try {
    // fetch 〜 JSON 〜 成功処理
  } catch (error) {
    // エラー処理
  } finally {
    endLoading();
  }
}
JavaScript

ここでのポイントは、
「ローディング中も resultDiv を空にしない」
ということです。

ユーザーにとっては、
「何もない」より「読み込み中の形が見える」方が安心感があります。


エラーハンドリングを「パターン」として整理する

エラー表示の種類を揃える

4日目では、エラー表示を少しだけ整理して、
「どの種類のエラーでも同じ関数を通る」ようにします。

function setStatus(message, type) {
  statusDiv.textContent = message;
  statusDiv.className = "status " + type; // CSS で色を変える
}

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

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

function showLoadingStatus() {
  setStatus("ニュースを取得中です…", "loading");
}
JavaScript

CSS 側で例えばこうしておきます。

.status.loading {
  color: #555;
}

.status.success {
  color: #0a0;
}

.status.error {
  color: #c00;
}

fetchNewsWithCurrentState はこう書き換えられます。

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

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

  try {
    const url = buildUrl({
      keyword: currentKeyword,
      from: currentFrom,
      to: currentTo,
      sortBy: currentSortBy,
      page: currentPage,
      pageSize,
      source: currentSource
    });

    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(`ニュースの取得に成功しました。(ページ ${currentPage})`);
    renderArticles(data);
    updatePaginationInfo(data);

  } catch (error) {
    showError(`ページ ${currentPage} の取得に失敗しました。ネットワークを確認してください。`);
    console.error(error);

  } finally {
    endLoading();
  }
}
JavaScript

ここでの深掘りポイントは、
「エラーの種類は増えても、“見せ方”は一つの関数に集約する」
という設計です。


4日目の全体像をまとめる

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

  • fetch・async/await・エラーハンドリングの「型」はそのまま
  • NewsAPI の sources パラメータを使ってソース絞り込みを追加
  • 検索条件(キーワード・期間・並び順・ソース)を「状態」として保持
  • 記事ごとに「★ お気に入り」ボタンを付け、別枠に一覧表示
  • ローディング中にスケルトン表示を出して、体験を良くする
  • エラー表示を setStatus / showError / showStatus でパターン化

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


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

4日目の本質は、

  • fetch・async/await・エラーハンドリングはもう“土台”として固まっている
  • これからの差は「どんな状態を持ち、どう UI に反映するか」で決まる
  • API の結果(articles)とアプリの状態(favorites・currentSource など)は別物として扱う

という感覚です。

あなたはもう、
「API を叩いて結果を表示する人」ではなく、
“API を前提にアプリの体験を設計する人”になりつつあります。

次の 5 日目では、
このニュースアプリに「検索履歴」や「最近見た記事」などを加えて、
さらに“日常使いできるレベル”に近づけていきましょう。

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