概要
Next.js App Router では、ルートセグメントに loading.tsx を置くだけでページ全体のローディング UI が自動適用される。
内部的には page.tsx が <Suspense> でラップされるため、サーバーコンポーネントの非同期処理が完了するまで loading.tsx の内容が表示される。
コンポーネント単位で待機させたい場合は <Suspense fallback={...}> を直接使う。
インストール
# 追加インストールは不要
loading.tsx によるページ単位のローディング UI
// src/app/posts/loading.tsx
export default function Loading() {
return (
<div className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="animate-pulse rounded border p-4">
<div className="mb-2 h-4 w-1/3 rounded bg-gray-200" />
<div className="h-3 w-full rounded bg-gray-100" />
</div>
))}
</div>
);
}
// src/app/posts/page.tsx
async function getPosts() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
if (!res.ok) throw new Error("取得に失敗しました");
return res.json() as Promise<{ id: number; title: string; body: string }[]>;
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul className="space-y-4">
{posts.map((post) => (
<li key={post.id} className="rounded border p-4 text-sm">
<p className="font-medium">{post.title}</p>
<p className="text-gray-500">{post.body}</p>
</li>
))}
</ul>
);
}
Suspense によるコンポーネント単位のローディング UI
ページの一部だけを遅延させたい場合は <Suspense> を直接使う。
// src/app/dashboard/page.tsx
import { Suspense } from "react";
function SkeletonCard() {
return (
<div className="animate-pulse rounded border p-4">
<div className="mb-2 h-4 w-1/2 rounded bg-gray-200" />
<div className="h-3 w-full rounded bg-gray-100" />
</div>
);
}
async function RecentPosts() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=3");
const posts = (await res.json()) as { id: number; title: string }[];
return (
<ul className="space-y-2">
{posts.map((post) => (
<li key={post.id} className="rounded border px-4 py-2 text-sm">
{post.title}
</li>
))}
</ul>
);
}
export default function DashboardPage() {
return (
<div className="space-y-6">
<h1 className="text-lg font-bold">ダッシュボード</h1>
<section>
<h2 className="mb-2 text-sm font-medium text-gray-600">最近の投稿</h2>
<Suspense fallback={<SkeletonCard />}>
<RecentPosts />
</Suspense>
</section>
</div>
);
}
ポイント
loading.tsxを置くだけでルートセグメント全体にローディング UI が適用される(page.tsxが自動的に<Suspense>でラップされる)<Suspense fallback={...}>はコンポーネント単位で使え、ページの一部だけを遅延させられる- スケルトン UI は
animate-pulseと背景色だけで実装できる(外部ライブラリ不要) loading.tsxは同一セグメントのlayout.tsxの中で機能するため、ヘッダーやナビゲーションはローディング中も表示され続ける