3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【09】Next.js app routerのチュートリアルやってみる(loading.tsxとSuspenseでストリーミング)

Last updated at Posted at 2024-02-06

はじめに

Next.js app routerのチュートリアルの第9章のアウトプットします。

前の記事

【01】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/af58da3d20cbc790e767

【02】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/edf450b3ee135e83d1e8

【03】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/612221eac233aa9cbb74

【04】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/62f9beccbfe36eaf7f90

【05】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/8b71b1d1df7c9435a9c9

【06】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/58130c3cfbaf8a573de2

【07】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/2c2da0f8071e60454679

【08】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/45f45fcb9cc14506f79f

第9章 ストリーミング

この章では下記を学習しました。

  • ストリーミング
  • ページ全体をストリーミングするloading.tsx
  • ルートグループ
  • コンポーネントをストリーミングするSuspense
  • ローディングスケルトン

ストリーミング

今回の場合ストリーミングとは、ページ全体を部品に分けた時、準備が整った部品から段階的にクライアント側に表示するという転送技術のことを指します。

ストリーミングすることで、遅いデータ要求によってページ全体がブロックされるのを防ぐことができます。

これにより、ユーザーはすべてのデータが読み込まれるのを待たずに、ページの一部を表示して操作できるようになります。

loading.tsx

loading.tsxストリーミングの中でも、ページ全体をストリーミングできるものです。

試しに、/app/dashboard/laoding.tsxファイルをつくるとします。

/app/dashboard/laoding.tsx
import DashboardSkeleton from '@/app/ui/skeletons'

export default function Loading() {
    return <div>Loading...</div>
}

すると、/dashboardにアクセスした際にLoading...という文字が表示されます。

そして、読み込みが完了するとダッシュボードを表示します。

image.png

  • loading.tsxNext.jsにおける特別なファイルで、ページの読み込み中に代替として表示するフォールバックUIを作成できます。

  • <SideNav />は静的レンダリングであるため、すぐに表示されますし、クリックすることもできます。

ルートグループ

現時点では、ディレクトリは以下のようになっています。

.
└── app/
    └── dashboard/
        ├── loading.tsx
        ├── page.tsx
        ├── layout.tsx
        ├── customers/
        │   └── page.tsx
        └── invoices/
            └── page.tsx

ここで問題が一つ発生しており、loading.tsxLoadin...という文字は以下の3つのファイルに適用さてしまうのです。

  • /dashboard/page.tsx
  • /dashboard/customers/page.tsx
  • /dashboard/invoices/page.tsx
.
└── app/
    └── dashboard/
        ├── loading.tsx
        ├── page.tsx -- Loading...表示される
        ├── layout.tsx 
        ├── customers/
        │   └── page.tsx -- Loading...表示される
        └── invoices/
            └── page.tsx -- Loading...表示される

これを/dashboard/page.tsxだけに適用させたいときに使うのがルートグループです。

ディレクトリ構成を以下のようにしてください。

.
└── app/
    └── dashboard/
        ├── layout.tsx
        ├── (overview)/
        │   ├── loading.tsx
        │   └── page.tsx
        ├── customers/
        │   └── page.tsx
        └── invoices/
            └── page.tsx

/dashboard直下のloading.tsxpage.tsx(overview)というフォルダの中に入れました。

これにより、loading.tsxは、ダッシュボードの概要ページにのみ反映されます。

()を使用して、フォルダを作成すると、URLのパス構造に影響を与えることなく論理グループに分けることができます。

そのため、ダッシュボードへのURLは/dashboard/(overview)/page.tsxではなく、

今まで通り、/dashboardでアクセスできます。

.
└── app/
    └── dashboard/
        ├── layout.tsx
        ├── (overview)/
        │   ├── loading.tsx
        │   └── page.tsx -- このファイルだけ Loading... を表示
        ├── customers/
        │   └── page.tsx
        └── invoices/
            └── page.tsx

Suspense

先ほどはページ全体をストリーミングする方法を学びました。

次は、コンポーネント単位でストリーミングを実装しましょう。

そのために使用するのが、Suspenseです。

RevenuChartコンポーネントをSuspenseでストリーミングする例を見てみましょう。


page.tsxrevenueのデータをフェッチする機能を削除します。

そして、Suspenseimportし、RevenueChartコンポーネントを囲っています。

fallbackには読み込み中に表示したいUIを設定できます。

/app/dashboard/page.tsx
import RevenueChart from '@/app/ui/dashboard/revenue-chart'
- import { fetchRevenue } from '../../lib/data'
+ import { Suspense } from 'react'

export default async function Page() {
-   const revenue = await fetchRevenue
    return (
        <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
-           <RevenueChart revenue={revenue} />
+           <Suspense fallback={<div>Loading...</div>}>
+               <RevenueChart />
+           </Suspense>
        </div>
    )
}

そして、RevenueChartコンポーネントがあるrevenu-chart.tsx側で、データフェッチを行います。

そのためにasyncをつけてコンポーネントを非同期にし、

await fetchRevenue()revenueを取得しています。

/app/ui/dashboard/revenue-chart.tsx
import { generateYAxis } from '@/app/lib/utils';
import { CalendarIcon } from '@heroicons/react/24/outline';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue } from '@/app/lib/data';
 
// ...
 
+export default async function RevenueChart() {
+ const revenue = await fetchRevenue();
 
  const chartHeight = 350;
  const { yAxisLabels, topLabel } = generateYAxis(revenue);
 
  if (!revenue || revenue.length === 0) {
    return <p className="mt-4 text-gray-400">No data available.</p>;
  }
 
  return (
    // ...
  );
}
 

この変更によってグラフが表示されるまでの間はLoading...という文字が表示されるようになりました。

image.png

↑ グラフはまだ読み込まれていないため、 Loading...が表示される↑

image.png

↑ 読み込まれるとLoading...という文字が消え、グラフが表示される ↑

コンポーネントのグループ化

下のCardコンポーネントもSuspenseで囲ってストリーミングを実装したいです。

/app/dashboard/page.tsx

import { Card } from '@/app/ui/dashboard/cards'
import { fetchCardData } from '../../lib/data'

export default async function Page() {
    const {
        numberOfCustomers,
        numberOfInvoices,
        totalPaidInvoices,
        totalPendingInvoices,
    } = await fetchCardData()
    return (
        <main>
            <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
                <Card
                    title="Collected"
                    value={totalPaidInvoices}
                    type="collected"
                />
                <Card
                    title="Pending"
                    value={totalPendingInvoices}
                    type="pending"
                />
                <Card
                    title="Total Invoices"
                    value={numberOfInvoices}
                    type="invoices"
                />
                <Card
                    title="Total Customers"
                    value={numberOfCustomers}
                    type="customers"
                />
            </div>
        </main>
    )
}

ですが、Cardコンポーネント一つずつをSuspenseで囲ってしまうと、

4つのカードがバラバラに表示されてユーザ-体験がよろしくありません。

そこで、コンポーネントをグループ化して、Suspenseを導入しましょう。

データフェッチの処理を削除して、4つのCardCardWrapperにしましょう。

/app/dashboard/page.tsx
import CardWrapper from '@/app/ui/dashboard/cards'
+import { Suspense } from 'react'

export default async function Page() {
    return (
        <main>
            <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
+                <Suspense fallback={<div>Loading...</div>}>
+                    <CardWrapper />
+                </Suspense>
            </div>
        </main>
    )
}

CardWrapper側にデータフェッチ処理を移します。

/app/ui/dashboard/cards.tsx
export default async function CardWrapper() {
    const {
        numberOfCustomers,
        numberOfInvoices,
        totalPaidInvoices,
        totalPendingInvoices,
    } = await fetchCardData()
    return (
        <>
            {/* NOTE: comment in this code when you get to this point in the course */}

            <Card
                title="Collected"
                value={totalPaidInvoices}
                type="collected"
            />
            <Card title="Pending" value={totalPendingInvoices} type="pending" />
            <Card
                title="Total Invoices"
                value={numberOfInvoices}
                type="invoices"
            />
            <Card
                title="Total Customers"
                value={numberOfCustomers}
                type="customers"
            />
        </>
    )
}

これによってCardコンポーネントもストリーミングが実装できました。

ローディングスケルトン

これで完了!かと思いきや、下の動画を見てください。

test.gif

グラフが表示される前にLoading...という文字が表示されるのはいいのですが、

グラフが表示される予定の場所を確保していないため、Latest InvoicesのUIが最初は左に寄っていて、

グラフが表示されると、右に寄るという挙動になってしまいます。


これを解決するのがローディングスケルトンです。

文字で説明するより見た方が早いので下の動画を見てください。

test.gif

UIを表示する予定の場所にフレームが表示されていますね。

これがローディングスケルトンです。

コードとしては、fallbackのところにローディングスケルトンのUIを渡しています。

/app/dashboard\page.tsx
<main>
    <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
        Dashboard
    </h1>
    <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
        <Suspense fallback={<div>Loading...</div>}>
            <CardWrapper />
        </Suspense>
    </div>
    <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
        <Suspense fallback={<div>Loading...</div>}>
            <RevenueChart />
        </Suspense>
        <Suspense fallback={<div>Loading...</div>}>
            <LatestInvoices />
        </Suspense>
    </div>
</main>

ローディングスケルトンのUI自体は自分でスタイリングする必要があるので、結構大変そうですね。

なにか楽にできる方法ないのかな。

Cardコンポーネントのローディングスケルトンの例
// Loading animation
const shimmer =
    'before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/60 before:to-transparent'

export function CardSkeleton() {
    return (
        <div
            className={`${shimmer} relative overflow-hidden rounded-xl bg-gray-100 p-2 shadow-sm`}
        >
            <div className="flex p-4">
                <div className="h-5 w-5 rounded-md bg-gray-200" />
                <div className="ml-2 h-6 w-16 rounded-md bg-gray-200 text-sm font-medium" />
            </div>
            <div className="flex items-center justify-center truncate rounded-xl bg-white px-4 py-8">
                <div className="h-7 w-20 rounded-md bg-gray-200" />
            </div>
        </div>
    )
}

おわりに

最後にNext.jsのSuspenseについての考え方を書いておきます。

サスペンスの境界をどこに置くかは、いくつかの要素によって決まります。

  • ストリーミング中にユーザーにページをどのように体験してもらいたいか。
  • どのコンテンツを優先したいか。
  • コンポーネントがデータの取得に依存している場合。

一般に、データのフェッチを必要なコンポーネントまで移動し、それらのコンポーネントを Suspense でラップすることをお勧めします。

ただし、アプリケーションが必要とする場合は、セクションまたはページ全体をストリーミングしても問題はありません。

Suspense は、より快適なユーザー エクスペリエンスを作成するのに役立つ強力な API なので、遠慮せずに試して何が最適かを確認してください。

次の記事

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?