3日目のゴールと今日やること
3日目のテーマは
「同じ fetch・async/await・エラーハンドリングの型を保ったまま、“ページネーション付きニュースアプリ”に育てる」
ことです。
1〜2日目で、あなたはすでに
- キーワード検索で NewsAPI から記事を取得する
- fetch を async/await で扱う
- response.ok と NewsAPI 独自のエラー(status: “error”)を分岐する
- ローディング状態とボタン制御を入れる
という「ニュース API 通信の基本フォーム」を手に入れました。
3日目はここに、
- ページネーション(次のページ / 前のページ)
- 現在ページの状態管理
- fetch の再利用(同じ関数でページだけ変える)
- 通信失敗時の分岐を“ページ付き”で考える
を足して、「ニュースを読み進められるアプリ」にしていきます。
今日の完成イメージを先に描く
どんなアプリにしたいか
3日目のミニアプリは、こんな動きをします。
- キーワード・期間・並び順を指定して検索
- 1 ページあたり 10 件の記事を表示
- 「次へ」「前へ」ボタンでページを移動
- ページを変えるたびに同じ条件で API を再取得
- ローディング中はボタンを無効化
- 通信失敗時はエラーメッセージを表示し、ページ番号は変えない
つまり、
「検索条件」と「ページ番号」をセットで扱う」
というのが今日の一番のテーマです。
まず「状態」を決める:何を覚えておく必要があるか
ページネーションに必要な状態
ページ付きのニュース検索には、最低限これが必要です。
- 現在のキーワード
- 現在の from / to / sortBy
- 現在のページ番号
- 1 ページあたりの件数(pageSize)
これをコード上の「状態」として持ちます。
let currentKeyword = "";
let currentFrom = "";
let currentTo = "";
let currentSortBy = "publishedAt";
let currentPage = 1;
const pageSize = 10;
JavaScriptここで大事なのは、
「fetch のたびにフォームから読み取る」のではなく、
一度検索した条件を状態として保持する
という発想です。
NewsAPI のページネーションの仕組みを理解する
page と pageSize の関係
NewsAPI の /everything は、
次のようなパラメータを受け取れます。
pageSize: 1 ページあたり何件か(最大 100)page: 何ページ目か(1 始まり)
例えば、
pageSize=10&page=1 → 1〜10件目
pageSize=10&page=2 → 11〜20件目
というイメージです。
buildUrl に page と pageSize を足す
2日目の buildUrl を拡張します。
const API_KEY = "YOUR_API_KEY";
const baseUrl = "https://newsapi.org/v2/everything";
function buildUrl({ keyword, from, to, sortBy, page, pageSize }) {
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);
}
return `${baseUrl}?${params.toString()}`;
}
JavaScriptここでのポイントは、
「ページ番号も URL の一部である」
ということです。
中心となる async 関数を「ページ対応」にする
fetchNews を「状態を使う関数」に変える
2日目までは、fetchNews にキーワードなどを渡していました。
3日目では、「状態を前提に動く関数」にします。
const keywordInput = document.getElementById("keywordInput");
const fromInput = document.getElementById("fromInput");
const toInput = document.getElementById("toInput");
const sortSelect = document.getElementById("sortSelect");
const searchButton = document.getElementById("searchButton");
const statusDiv = document.getElementById("status");
const resultDiv = document.getElementById("result");
let isLoading = false;
let currentKeyword = "";
let currentFrom = "";
let currentTo = "";
let currentSortBy = "publishedAt";
let currentPage = 1;
const pageSize = 10;
function validateKeyword(keyword) {
if (!keyword.trim()) return "キーワードを入力してください。";
if (keyword.length > 50) return "キーワードが長すぎます。50文字以内で入力してください。";
return null;
}
function showError(message) {
statusDiv.textContent = message;
statusDiv.style.color = "red";
}
function showStatus(message) {
statusDiv.textContent = message;
statusDiv.style.color = "black";
}
function startLoading() {
isLoading = true;
searchButton.disabled = true;
nextButton.disabled = true;
prevButton.disabled = true;
}
function endLoading() {
isLoading = false;
searchButton.disabled = false;
updatePaginationButtons();
}
JavaScriptここで nextButton と prevButton は、後で HTML に追加するページ移動ボタンです。
検索開始時に「状態を更新」する
検索ボタンを押したときは、
フォームの値を「現在の状態」にコピーします。
searchButton.addEventListener("click", () => {
const keyword = keywordInput.value.trim();
const from = fromInput.value;
const to = toInput.value;
const sortBy = sortSelect.value;
const error = validateKeyword(keyword);
if (error) {
showError(error);
resultDiv.textContent = "";
return;
}
currentKeyword = keyword;
currentFrom = from;
currentTo = to;
currentSortBy = sortBy;
currentPage = 1;
fetchNewsWithCurrentState();
});
JavaScriptここでのポイントは、
「フォーム → 状態 → fetch」
という流れを作っていることです。
状態を使って API を叩く関数
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("通信に失敗しました。ネットワークを確認してください。");
console.error(error);
} finally {
endLoading();
}
}
JavaScriptここで重要なのは、
「ページが変わっても、fetch の書き方は一切変わらない」
ということです。
ページネーション UI を実装する
HTML にボタンと情報表示を追加する
<div id="pagination">
<button id="prevButton">前のページ</button>
<span id="pageInfo"></span>
<button id="nextButton">次のページ</button>
</div>
JavaScript で要素を取得します。
const prevButton = document.getElementById("prevButton");
const nextButton = document.getElementById("nextButton");
const pageInfo = document.getElementById("pageInfo");
JavaScripttotalResults から「最大ページ数」を計算する
NewsAPI のレスポンスには、totalResults というプロパティがあります。
{
"status": "ok",
"totalResults": 123,
"articles": [ ... ]
}
これを使って、最大ページ数を計算します。
let totalResults = 0;
let maxPage = 1;
function updatePaginationInfo(data) {
totalResults = data.totalResults || 0;
maxPage = Math.max(1, Math.ceil(totalResults / pageSize));
pageInfo.textContent = `ページ ${currentPage} / ${maxPage}(全 ${totalResults} 件)`;
updatePaginationButtons();
}
function updatePaginationButtons() {
prevButton.disabled = isLoading || currentPage <= 1;
nextButton.disabled = isLoading || currentPage >= maxPage;
}
JavaScript「次へ」「前へ」ボタンの動き
nextButton.addEventListener("click", () => {
if (isLoading) return;
if (currentPage >= maxPage) return;
currentPage += 1;
fetchNewsWithCurrentState();
});
prevButton.addEventListener("click", () => {
if (isLoading) return;
if (currentPage <= 1) return;
currentPage -= 1;
fetchNewsWithCurrentState();
});
JavaScriptここでのポイントは、
- ページ番号を変えるのはボタン側
- API を叩くのは常に
fetchNewsWithCurrentState - 「状態を変える」と「通信する」を分けて考えている
という構造です。
通信失敗時の分岐を「ページ付き」で考える
失敗したときにページ番号をどうするか
ここが地味に大事なポイントです。
例えば「次のページへ」を押してcurrentPage を 2 → 3 にしたあと、
通信が失敗したとします。
このとき、
- ページ番号を 3 のままにするか
- 2 に戻すか
という選択があります。
今回は「ユーザーが押した事実を尊重して、ページ番号はそのまま」にしておきます。
理由は、再試行したときに「同じページをもう一度取りに行く」方が自然だからです。
もし「戻したい」場合は、
catch の中で currentPage -= 1 する、という設計もありです。
エラーメッセージを少しだけ丁寧にする
ページ付きのアプリでは、
エラーメッセージにページ番号を含めるのもアリです。
} catch (error) {
showError(`ページ ${currentPage} の取得に失敗しました。ネットワークを確認してください。`);
console.error(error);
}
JavaScriptこうすると、
「今どのページで失敗したのか」がユーザーにもわかります。
3日目の完成コード(重要部分をまとめて)
const API_KEY = "YOUR_API_KEY";
const baseUrl = "https://newsapi.org/v2/everything";
const keywordInput = document.getElementById("keywordInput");
const fromInput = document.getElementById("fromInput");
const toInput = document.getElementById("toInput");
const sortSelect = document.getElementById("sortSelect");
const searchButton = document.getElementById("searchButton");
const statusDiv = document.getElementById("status");
const resultDiv = document.getElementById("result");
const prevButton = document.getElementById("prevButton");
const nextButton = document.getElementById("nextButton");
const pageInfo = document.getElementById("pageInfo");
let isLoading = false;
let currentKeyword = "";
let currentFrom = "";
let currentTo = "";
let currentSortBy = "publishedAt";
let currentPage = 1;
const pageSize = 10;
let totalResults = 0;
let maxPage = 1;
function validateKeyword(keyword) {
if (!keyword.trim()) return "キーワードを入力してください。";
if (keyword.length > 50) return "キーワードが長すぎます。50文字以内で入力してください。";
return null;
}
function buildUrl({ keyword, from, to, sortBy, page, pageSize }) {
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);
return `${baseUrl}?${params.toString()}`;
}
function showError(message) {
statusDiv.textContent = message;
statusDiv.style.color = "red";
}
function showStatus(message) {
statusDiv.textContent = message;
statusDiv.style.color = "black";
}
function startLoading() {
isLoading = true;
searchButton.disabled = true;
prevButton.disabled = true;
nextButton.disabled = true;
}
function endLoading() {
isLoading = false;
searchButton.disabled = false;
updatePaginationButtons();
}
function renderArticles(data) {
const articles = data.articles;
if (!articles || articles.length === 0) {
resultDiv.textContent = "該当するニュースが見つかりませんでした。";
return;
}
let html = "";
articles.forEach((article) => {
const title = article.title || "タイトルなし";
const description = article.description || "説明文はありません。";
const url = article.url;
const source = article.source?.name || "不明なソース";
html += `
<div class="article">
<h3>${title}</h3>
<p>${description}</p>
<p>提供元:${source}</p>
<p><a href="${url}" target="_blank" rel="noopener noreferrer">記事を読む</a></p>
</div>
`;
});
resultDiv.innerHTML = html;
}
function updatePaginationInfo(data) {
totalResults = data.totalResults || 0;
maxPage = Math.max(1, Math.ceil(totalResults / pageSize));
pageInfo.textContent = `ページ ${currentPage} / ${maxPage}(全 ${totalResults} 件)`;
updatePaginationButtons();
}
function updatePaginationButtons() {
prevButton.disabled = isLoading || currentPage <= 1;
nextButton.disabled = isLoading || currentPage >= maxPage;
}
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();
}
}
searchButton.addEventListener("click", () => {
const keyword = keywordInput.value.trim();
const from = fromInput.value;
const to = toInput.value;
const sortBy = sortSelect.value;
const error = validateKeyword(keyword);
if (error) {
showError(error);
resultDiv.textContent = "";
return;
}
currentKeyword = keyword;
currentFrom = from;
currentTo = to;
currentSortBy = sortBy;
currentPage = 1;
fetchNewsWithCurrentState();
});
nextButton.addEventListener("click", () => {
if (isLoading) return;
if (currentPage >= maxPage) return;
currentPage += 1;
fetchNewsWithCurrentState();
});
prevButton.addEventListener("click", () => {
if (isLoading) return;
if (currentPage <= 1) return;
currentPage -= 1;
fetchNewsWithCurrentState();
});
JavaScript今日いちばん深く理解してほしいこと
3日目で本当に掴んでほしいのは、この感覚です。
- fetch・async/await・エラーハンドリングの「型」はもう変えない
- 変わるのは「状態(currentPage など)」と「URL のパラメータ」だけ
- 状態を中心に設計すると、ページネーションも自然に書ける
天気 API でも、ニュース API でも、
「ページ付き一覧を表示する」というのは超よくあるパターンです。
あなたはもう、
「API から一覧を取って、ページをまたいで読ませるアプリ」を自力で組み立てられる人
のところまで来ています。

