React / Next.js公開 約11分

Next.js loadingとerrorで待機・失敗・該当なしを分ける

データを待つ間も、一覧を開けない場合も、同じ空画面にしていませんか。App Routerのファイル配置から表示範囲を決めます。遅延と二種類の終了状態を再現し、共通ナビと再試行の動作を確認します。

この記事の目次
  1. 資料棚の外枠を、三つの表示の外へ置く
  2. 状態をファイルで切り替える資料棚を作る
  3. 待機と例外が届かないときは、囲まれる範囲を見る
  4. 再試行は、原因が変わった後にも試す
  5. 三つの出口と、戻るリンクを照合する

一覧を開いても反応が見えず、しばらくすると「資料がありません」と表示された。それが読込み中なのか、本当に資料がないのか、取得に失敗したのかで、利用者が次に取る行動は変わります。App Routerでは、それぞれの表示を置くファイルと範囲を決められます。

ReactのJSX、async関数、Server ComponentとClient Componentの区別が分かる人向けです。架空の資料棚をNext.js 16.4.5、React / React DOM 19.2.0、Node.js 24系で作ります。App Routerを使い、Cache Componentsは無効にします。実際のAPI通信は加えず、ローカルの模擬データで表示経路を確かめます。

資料棚の外枠を、三つの表示の外へ置く

今回の/catalogでは、共通の見出しと戻るリンクを残し、その下だけを切り替えます。待機はloading.jsx、予期しない例外はerror.jsx、資料が存在しない結果はnot-found.jsxへ割り当てます。全部を失敗と呼ぶと、待つべき場面と戻るべき場面が区別できません。

ファイル 表示する条件 共通の外枠
app/catalog/loading.jsx pageの処理を待っている 残る
app/catalog/page.jsx 正常に資料が見つかった 残る
app/catalog/error.jsx 内側で未処理の例外が起きた 残る
app/catalog/not-found.jsx notFoundを呼んだ 残る

外枠は同じフォルダのlayout.jsxへ置きます。loadingの公式資料では、pageや子の領域をSuspenseという待機境界で囲み、同じ階層のlayout自身は囲まない構造を確認できます。この配置を知ると、読み込みを待っても残る範囲を決められます。

状態をファイルで切り替える資料棚を作る

空フォルダで次を実行します。練習用のstate.jsonだけを編集して条件を変えます。

npm init -y
npm install --save-exact next@16.4.5 react@19.2.0 react-dom@19.2.0
mkdir -p app/catalog data

next.config.mjsとdata/state.jsonを、それぞれ保存します。

export default { cacheComponents: false };
{ "state": "ok" }

app/layout.jsxは文書全体の外枠です。読みやすさのための最小限の余白だけを付けます。

export const metadata = { title: '資料棚の状態確認' };
export default function Layout({ children }) {
  return <html lang="ja"><body style={{ margin: 16, fontFamily: 'sans-serif', lineHeight: 1.8 }}>
    <main style={{ maxWidth: 560, margin: 'auto' }}>{children}</main>
  </body></html>;
}

app/page.jsxに入口を作ります。prefetchを無効にし、この練習ではクリック後の待機を観察しやすくしています。

import Link from 'next/link';
export default function Page() {
  return <><h1>状態の確認</h1><p>架空の資料棚を開く練習です。</p>
    <Link href="/catalog" prefetch={false}>資料棚を開く</Link></>;
}

app/catalog/layout.jsxに、読み込み中も残したい案内を書きます。

import Link from 'next/link';
export default function CatalogLayout({ children }) {
  return <><h1>資料棚</h1><nav aria-label="練習の移動">
    <Link href="/">状態の確認へ戻る</Link>
  </nav>{children}</>;
}

app/catalog/page.jsxが読取りを担当します。connectionの後に1.2秒待ち、ファイルを読みます。connectionの資料に沿って要求時に実行し、ビルド時の一回の読取りで固定しない構成です。

import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { connection } from 'next/server';
import { notFound } from 'next/navigation';

export default async function CatalogPage() {
  await connection();
  await new Promise(resolve => setTimeout(resolve, 1200));
  const raw = await readFile(path.join(process.cwd(), 'data/state.json'), 'utf8');
  const { state } = JSON.parse(raw);
  if (state === 'missing') notFound();
  if (state !== 'ok') throw new Error('Practice catalog failure');
  return <section><h2>公開中の資料</h2><p>水辺の観察地図</p></section>;
}

順にapp/catalog/loading.jsx、app/catalog/error.jsx、app/catalog/not-found.jsxへ保存します。errorだけはClient Componentです。

export default function Loading() {
  return <p role="status">資料を読み込んでいます。</p>;
}
'use client';
export default function CatalogError({ retry }) {
  return <section><h2>資料を開けませんでした</h2>
    <p>時間をおいて再試行するか、上のリンクから戻ってください。</p>
    <button type="button" onClick={() => retry()}>もう一度読み込む</button>
  </section>;
}
export default function NotFound() {
  return <section><h2>該当する資料はありません</h2>
    <p>上のリンクから状態の確認へ戻れます。</p></section>;
}

公開用の動作を調べるため、npx next buildが成功した後、npx next start --hostname 127.0.0.1 --port 3007で起動します。http://127.0.0.1:3007から資料棚へ進むと、待機の文が資料名へ置き換わります。見え方の時間は通信や描画にも左右され、画面に正確に1.2秒出る保証ではありません。

待機と例外が届かないときは、囲まれる範囲を見る

第一の失敗は、pageの待機や読取りを同階層のcatalog/layout.jsxへ移すことです。catalog/loading.jsxはそのlayoutを囲まないため、そこに置いた待機を受け持てません。修正は、資料の読取りをpageへ戻すことです。入口から入り直し、外枠が読める間に待機の文が出るか再確認します。

errorの境界にも同様の範囲があります。同階層のlayoutで発生した例外は、そのerror.jsxでは扱えません。errorの公式資料に従い、外側の境界が必要な処理か、内側へ置く処理かを判断します。今回の見本は資料読取りを内側へ限定しています。

第二の失敗は、page全体をtry/catchで囲み、catchでは必ず通常のErrorを投げることです。notFoundも描画を中断する仕組みを使うため、広いcatchで捕まえると「該当なし」を取得失敗へ変えてしまいます。修正ではnotFoundの呼出しをそのcatchの外へ置きます。完成例は一括のcatchを設けていません。missingを入れて、再試行ボタンのない案内へ届くか確かめます。

再試行は、原因が変わった後にも試す

state.jsonを{ "state": "error" }へ変え、/catalogを再読み込みします。資料を開けない案内とボタンが出るはずです。そのまま再試行しても原因が残っているので、再びエラーになります。ボタンがあることと、復旧できることは別です。

次にファイルをokへ戻し、ページを手動で再読み込みせず「もう一度読み込む」を押します。16.4.5のretryは再取得と再描画を試みるので、資料名へ復帰するのが期待結果です。resetはエラー状態を解除して再描画する用途で、再取得を含む今回の操作にはretryを使います。古いバージョンへこのprops名をそのまま移しません。

error.messageをそのまま利用者へ表示する構成にもしていません。公開環境のServer Componentのエラー詳細は伏せられます。画面には次の操作を示し、原因はサーバー側の記録で調べます。クリックイベント内の任意の例外や入力値の不正まで、error.jsxがすべて処理するわけではありません。

三つの出口と、戻るリンクを照合する

15〜20分の確認では、次の表を上から試します。state.jsonは起動中でも読み直される教材ですが、実サービスの保存やキャッシュの更新手順を示す例ではありません。その課題はNext.jsのキャッシュの記事で別に扱います。

条件と操作 期待する最終表示
okで入口から移動 待機の後に「水辺の観察地図」
errorで/catalogを再読み込み 失敗の文と再試行ボタン
表示を残したままokへ戻し、再試行 資料名へ復帰
missingで/catalogを再読み込み 該当なしの文、再試行ボタンなし

各状態で「資料棚」と戻るリンクが残り、TabとEnterで移動・再試行できるかも確認します。最後はokへ戻します。公開・運用の無料レッスンの確認記録にも、表示と操作を一組で残せます。

not-foundの資料が説明するとおり、ストリーミング開始後の該当なしはHTTP 200のまま表示される場合があります。この教材でも見出しとnoindexの指定を含めて確認し、「該当なし画面が出たからHTTP 404」と決め付けないでください。状態ごとの文と、利用者が戻れる経路がそろったところで確認を終えます。

TRY A LESSON

記事の内容を、課題で確かめる

前提になるJavaScriptの練習 →

React・TypeScriptの学習内容 →

記事についてのお問い合わせ

記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。

← 記事一覧に戻る