前の記事
次の記事
フェーズ5-12:エラーハンドリング改善
今回は、APIやDBに接続できない場合にNext.jsの赤いRuntime Errorをそのまま見せず、
データを取得できませんでした。
サーバーとの通信中にエラーが発生しました。
しばらくしてから再度お試しください。
[ 再試行 ]
のような画面を表示する。
Next.js App Routerでは、特殊ファイルerror.tsxを配置すると、そのルート配下で発生した未処理例外を捕捉できる。
error.tsxを作成
src/app/error.tsx
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import styles from "./error.module.css";
export default function Error({
error,
reset,
}: {
error: Error & {
digest?: string;
};
reset: () => void;
}) {
// App Routerを操作するために取得
const router = useRouter();
// エラー内容は開発者向けにログへ出力する
useEffect(() => {
console.error(
"画面表示中にエラーが発生しました。",
error
);
}, [error]);
/**
* 再試行処理
*
* 1. error.tsxのError Boundaryをリセット
* 2. 現在のルートをサーバーから再取得
*/
const handleRetry = () => {
reset();
router.refresh();
};
return (
<main className={styles.container}>
<div className={styles.card}>
<h1 className={styles.title}>
データを取得できませんでした
</h1>
<p className={styles.message}>
サーバーとの通信中にエラーが発生しました。
</p>
<p className={styles.message}>
しばらくしてから再度お試しください。
</p>
<button
type="button"
className={styles.retryButton}
onClick={handleRetry}
>
再試行
</button>
</div>
</main>
);
}
エラー画面用CSS
src/app/error.module.css
/* =========================================
エラー画面全体
========================================= */
.container {
min-height: 70vh;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
}
/* =========================================
エラーカード
========================================= */
.card {
width: 100%;
max-width: 520px;
padding: 32px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: white;
text-align: center;
}
/* =========================================
タイトル
========================================= */
.title {
margin-top: 0;
margin-bottom: 20px;
font-size: 24px;
}
/* =========================================
説明文
========================================= */
.message {
margin: 8px 0;
color: #666;
line-height: 1.6;
}
/* =========================================
再試行ボタン
========================================= */
.retryButton {
margin-top: 24px;
padding: 10px 20px;
border: none;
border-radius: 6px;
background-color: #333;
color: white;
font-size: 15px;
cursor: pointer;
}
/* マウスを乗せた場合 */
.retryButton:hover {
opacity: 0.85;
}
動作確認
わざとAPIを停止して確認する。
DockerでASP.NET Core APIを停止する。
ブラウザで再試行の画面が表示されることを確認。
その後、APIを起動して、再試行を押すと復旧することを確認。
フェーズ5-13:ローディング表示
Next.jsのloading.tsxは、そのルートのpage.tsxがデータ取得などで待っている間に表示する特殊ファイル。
内部的にはReactのSuspenseを利用している。
これまでは、
ページへ移動
↓
getProcesses()
↓
API待ち
↓
画面完成
↓
表示
だったのを、loading.tsxがあると、
ページへ移動
↓
┌───────────────────────┐
│ データを読み込んでいます... │
└───────────────────────┘
↓
裏でAPI取得
↓
取得完了
↓
実際のページに切り替え
共通ローディング画面を作る
src/app/loading.tsx
import styles from "./loading.module.css";
/**
* ページのデータ取得中に表示する画面。
*
* Server Componentのレンダリングや
* APIからのデータ取得を待っている間に表示される。
*/
export default function Loading() {
return (
<main className={styles.container}>
<div className={styles.content}>
{/* 読み込み中を表すアニメーション */}
<div className={styles.spinner} />
<p className={styles.message}>
データを読み込んでいます...
</p>
</div>
</main>
);
}
CSSを作成
src/app/loading.module.css
/* =========================================
ローディング画面全体
========================================= */
.container {
min-height: 70vh;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
}
/* =========================================
ローディング表示
========================================= */
.content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
/* =========================================
スピナー
========================================= */
.spinner {
width: 40px;
height: 40px;
border: 4px solid #ddd;
/* 上側だけ色を変えることで、
回転していることが分かるようにする */
border-top-color: #333;
border-radius: 50%;
/* spinnerAnimationを繰り返し実行する */
animation:
spinnerAnimation 0.8s linear infinite;
}
/* =========================================
スピナーの回転アニメーション
========================================= */
@keyframes spinnerAnimation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* =========================================
メッセージ
========================================= */
.message {
margin: 0;
color: #666;
font-size: 15px;
}
動作確認
画面を表示する際、「データを読み込んでいます」が表示されることを確認。
フェーズ5-14:共通ヘッダー・ナビゲーション
今回はNext.jsのRoute Group + nested layout.tsxを使う。
(authenticated)のように括弧で囲んだフォルダはURLに含まれないため、ファイルを移動しても / や /processesのURLは変わらない。
フォルダ構成を変更する
現在のログイン後画面を(authenticated)の中へ移動する。
src
└─ app
│
├─ (authenticated) ← 新規
│ ├─ layout.tsx ← 新規
│ ├─ app-header.tsx ← 新規
│ ├─ app-header.module.css ← 新規
│ │
│ ├─ page.tsx ← 現在の app/page.tsx を移動
│ ├─ dashboard.module.css ← 一緒に移動
│ │
│ └─ processes ← フォルダごと移動
│ ├─ page.tsx
│ ├─ processes.module.css
│ └─ [id]
│ └─ tasks
│ ├─ page.tsx
│ ├─ actions.ts
│ └─ tasks.module.css
│
├─ actions
│ └─ logout.ts ← そのまま
│
├─ login ← そのまま
│
├─ error.tsx
├─ error.module.css
├─ loading.tsx
├─ loading.module.css
├─ layout.tsx ← RootLayout
└─ globals.css
共通ヘッダーを作る
src/app/(authenticated)/app-header.tsx
import Link from "next/link";
import {
logoutAction,
} from "@/app/actions/logout";
import styles from "./app-header.module.css";
/**
* ログイン後の画面で共通表示するヘッダー。
*
* トップ・工程進捗へのナビゲーションと
* ログアウト機能をまとめる。
*/
export default function AppHeader() {
return (
<header className={styles.header}>
{/* =====================================
システム名
===================================== */}
<div className={styles.inner}>
<Link
href="/"
className={styles.logo}
>
工程進捗管理システム
</Link>
{/* ===================================
ナビゲーション
=================================== */}
<nav className={styles.nav}>
<Link
href="/"
className={styles.navLink}
>
ダッシュボード
</Link>
<Link
href="/processes"
className={styles.navLink}
>
工程進捗
</Link>
{/* =================================
ログアウト
================================= */}
<form action={logoutAction}>
<button
type="submit"
className={styles.logoutButton}
>
ログアウト
</button>
</form>
</nav>
</div>
</header>
);
}
ヘッダー用CSS
src/app/(authenticated)/app-header.module.css
/* =========================================
ヘッダー全体
========================================= */
.header {
border-bottom: 1px solid #ddd;
background-color: white;
}
/* =========================================
ヘッダー内部
========================================= */
.inner {
max-width: 1100px;
margin: 0 auto;
padding: 14px 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
/* =========================================
システム名
========================================= */
.logo {
color: #222;
font-size: 18px;
font-weight: bold;
text-decoration: none;
white-space: nowrap;
}
/* =========================================
ナビゲーション
========================================= */
.nav {
display: flex;
align-items: center;
gap: 16px;
}
/* ナビゲーションリンク */
.navLink {
color: #444;
text-decoration: none;
white-space: nowrap;
}
.navLink:hover {
text-decoration: underline;
}
/* =========================================
ログアウトボタン
========================================= */
.logoutButton {
padding: 8px 14px;
border: 1px solid #bbb;
border-radius: 6px;
background-color: white;
cursor: pointer;
}
.logoutButton:hover {
background-color: #f5f5f5;
}
/* =========================================
スマートフォン表示
========================================= */
@media (max-width: 600px) {
.inner {
align-items: flex-start;
flex-direction: column;
gap: 12px;
}
.nav {
width: 100%;
flex-wrap: wrap;
}
}
(authenticated)/layout.tsxを作成
src/app/(authenticated)/layout.tsx
import AppHeader from "./app-header";
/**
* ログイン後画面の共通レイアウト。
*
* このフォルダ配下のページには、
* AppHeaderが自動的に表示される。
*/
export default function AuthenticatedLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
{/* 共通ヘッダー */}
<AppHeader />
{/* 各画面 */}
{children}
</>
);
}
トップ画面からログアウトボタンを削除
src/app/(authenticated)/page.tsx
下記を削除する。
<form action={logoutAction}>
<button>
ログアウト
</button>
</form>
import {
logoutAction,
} from "./actions/logout";
動作確認
共通ヘッダーが表示されることを確認。
フェーズ5-15:レスポンシブ対応
スマートフォン表示に対応する。
ダッシュボード
src/app/(authenticated)/dashboard.module.css
の最後に追加する。
/* =========================================
スマートフォン表示
========================================= */
@media (max-width: 600px) {
/* 画面左右の余白を少し小さくする */
.container {
padding: 20px 16px;
}
/* タイトルを少し小さくする */
.title {
font-size: 24px;
}
/* サマリーカードを1列にする */
.summaryGrid {
grid-template-columns: 1fr;
gap: 12px;
}
/* カード内部の余白を少し小さくする */
.summaryCard {
padding: 18px;
}
/* 数値を少し小さくする */
.summaryValue {
font-size: 28px;
}
/* 工程進捗ボタンを横幅いっぱいにする */
.primaryLink {
display: block;
width: 100%;
box-sizing: border-box;
text-align: center;
}
}
工程一覧
src/app/(authenticated)/processes/processes.module.css
の最後に追加する。
/* =========================================
スマートフォン表示
========================================= */
@media (max-width: 600px) {
.container {
padding: 20px 16px;
}
.title {
font-size: 24px;
}
/* 工程カードを1列表示にする */
.grid {
grid-template-columns: 1fr;
}
.card {
padding: 16px;
}
.processName {
font-size: 18px;
}
/* ステータス件数を縦方向にも折り返せるようにする */
.statusList {
gap: 8px 12px;
}
/* 完了率部分 */
.progressArea {
gap: 8px;
}
}
作業一覧画面
src/app/(authenticated)/processes/[id]/tasks/tasks.module.css
の最後に追加する。
/* =========================================
タブレット・スマートフォン表示
========================================= */
@media (max-width: 768px) {
.container {
padding: 20px 16px;
}
.title {
font-size: 24px;
}
/* -----------------------------------------
テーブル
----------------------------------------- */
.tableWrapper {
/*
テーブルが画面幅を超えた場合、
横スクロールできるようにする。
*/
overflow-x: auto;
/*
iPhoneなどでスクロールを滑らかにする。
*/
-webkit-overflow-scrolling: touch;
}
.table {
/*
列が無理に押し縮められないよう、
最低幅を確保する。
*/
min-width: 720px;
}
.table th,
.table td {
padding: 10px 12px;
}
/* -----------------------------------------
ステータス変更ボタン
----------------------------------------- */
.buttonGroup {
gap: 4px;
}
.statusButton {
padding: 7px 10px;
font-size: 14px;
}
}
ステータスボタンを折り返さない
tasks.module.cssの.buttonGroupが、
.buttonGroup {
display: flex;
gap: 6px;
}
になっているので、そのままでも基本的に横並び。
念のため、
.buttonGroup {
display: flex;
gap: 6px;
flex-wrap: nowrap;
}
に変更する。
ログイン画面も確認
src/app/login/login.module.cssには、
.loginCard {
width: 100%;
max-width: 400px;
}
があるので、レスポンシブ対応できている。
ただしスマホ端でカードが画面いっぱいになり過ぎる可能性があるため、.containerに、
.container {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #f5f5f5;
padding: 16px;
}
と padding: 16px;を追加しておくと安全。
また.loginCardに、
box-sizing: border-box;
も追加する。
動作確認
Chromeでスマホ表示を確認する。
ChromeでF12を押す。
上部にあるスマホ・タブレットのアイコン、
Toggle device toolbar
を押す。