JavaScript PR

Next.jsに入門してみた(Chapter5〜8)【Next.js, React】

Next.jsに入門してみた(Chapter5~8)【Next.js, React】
記事内に商品プロモーションを含む場合があります

Next.jsに改めて入門しようと思い、公式サイトの「Learn Next.js」というチュートリアル(?)をやっていきます。

かなり長いチュートリアルだったので複数回に分け紹介しており、こちらの記事はChapter5〜Chapter8の内容になります。

Chapter1〜Chapter4までの内容は下記をご覧ください。

Next.jsに入門してみた(Chapter1〜4)【Next.js, React】
Next.jsに入門してみた(Chapter1〜4)【Next.js, React】こちらの記事ではNext.jsに入門するために、公式サイトのチュートリアルを翻訳し、僕自身の解釈や理解をまとめております。 Reactの基礎を学び終えて、Next.jsに入門したい方はぜひご覧ください。...

なお記事の内容は下記に注意してご覧くださいm(_ _)m

  • こちらの記事は2023年11月18日現在の情報になります。
  • 公式サイトのチュートリアルを翻訳し(主にGoogle翻訳を使わせていただきました)、僕自身の解釈や理解を伝えるものになります。
  • わかりやすくするために、翻訳通りの説明になっていない部分や、僕自身が調べて追加説明してある部分があります。
  • 公式に書かれていることを全て書いていませんし、公式の見解と一致しているとは限りません。

※間違え等あればご連絡くださいm(_ _)m

目次

環境・バージョン

  • Mac OS
  • Node.js 20.9.0(18よりも新しいものである必要がある)
  • VSCode

Chapter 5(Navigating Between Pages)

ここではダッシュボード以下のページ間を移動できるようにしていきます。

なぜナビゲーションを最適化するのか?(Why optimize navigation?)

現状、<SideNavi>コンポーネントでは、従来ページ間をリンクするときに使われている<a>要素を使っています。(正確には<SideNavi>コンポーネントがインポートしている<NavLinks>コンポーネント)

なので、ダッシュボードページや、請求書ページ、顧客ページに移動すると、ページ全体が更新されています。(リンクをクリックした後、タブ(下の画像の赤枠)を確認するとロードされているのがわかります)

<Link>コンポーネント(The component)

Next.jsではページ間移動の際、<Link/>コンポーネントを使えば、JavaScriptを使用した「client-side navigation」が実現できます。

下記のように「/app/ui/dashboard/nav-links.tsx」の<a>タグを<Link>に置き換えてましょう。

/app/ui/dashboard/nav-links.tsx
import {
  UserGroupIcon,
  HomeIcon,
  DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
 
// ...
 
export default function NavLinks() {
  return (
    <>
      {links.map((link) => {
        const LinkIcon = link.icon;
        return (
          <Link
            key={link.name}
            href={link.href}
            className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"
          >
            <LinkIcon className="w-6" />
            <p className="hidden md:block">{link.name}</p>
          </Link>
        );
      })}
    </>
  );
}

再度ダッシュボードのホームページや、請求書ページ、顧客ページに移動してみると、画面の一部分のみの更新になっています。(タブのロードがなくなっているはずです)

パターン: アクティブなリンクを表示する(Pattern: Showing active links)

ここでは一般的なUIのパターンである、アクティブリンクを実装します。(現在表示されているページを示すUI)

そのために、「usePathname()」というフックを使います。usePathname()はURL からユーザーの現在のパスを取得することができます。

また、usePathname()はフックであるため、使用するには下記のようにファイルの先頭で「use client」の記述をして、「next/navigation」からインポートします。

/app/ui/dashboard/nav-links.tsx
'use client';
 
import {
  UserGroupIcon,
  HomeIcon,
  InboxIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
 
// ...

※ちなみに、「use client」の記述をなくし、「http://localhost:3000/dashboard」へアクセスすると

Unhandled Runtime Error
Error: usePathname only works in Client Components. Add the “use client” directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component

というエラーが発生します。

チュートリアルの「use client」をつける必要性が書かれているような箇所の翻訳が少し不安なのですが、上記のエラーに書かれていた。こちらの公式サイトのurlを見ると下記のように書いてあるので、とにもかくにもhookを使う場合は、「use client」をつけようという認識でOKかと思います!

Mark the component using the hook as a Client Component by adding 'use client' at the top of the file.

(ファイルの先頭に「use client」を追加して、フックを使用するコンポーネントをクライアント コンポーネントとしてマークします。)

React client hook in Server Component | Next.js(https://nextjs.org/docs/messages/react-client-hook-in-server-component)

次に、<NavLinks />コンポーネント内で変数に割り当てます。

/app/ui/dashboard/nav-links.tsx
export default function NavLinks() {
  const pathname = usePathname();
  // ...
}

そして、CSSのスタイリングの章で紹介したclsxライブラリを下記のように使って実装します。

/app/ui/dashboard/nav-links.tsx
'use client';
 
import {
  UserGroupIcon,
  HomeIcon,
  DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import clsx from 'clsx';
 
// ...
 
export default function NavLinks() {
  const pathname = usePathname();
 
  return (
    <>
      {links.map((link) => {
        const LinkIcon = link.icon;
        return (
          <Link
            key={link.name}
            href={link.href}
            className={clsx(
              'flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3',
              {
                'bg-sky-100 text-blue-600': pathname === link.href,
              },
            )}
          >
            <LinkIcon className="w-6" />
            <p className="hidden md:block">{link.name}</p>
          </Link>
        );
      })}
    </>
  );
}

ダッシュボードの各ページに遷移し、表示したページのリンクが青色で強調されていればOKです。

Chapter 6(Setting Up Your Database)

Before you can continue working on your dashboard, you’ll need some data. In this chapter, you’ll be setting up a PostgreSQL database using @vercel/postgres.

(ダッシュボードでの作業を続ける前に、いくつかのデータが必要です。 この章では、@vercel/postgres を使用して PostgreSQL データベースをセットアップします。)

Learn Next.js: Setting Up Your Database | Next.js(https://nextjs.org/learn/dashboard-app/setting-up-your-database)

まず、GitHubのレポジトリを作り、プッシュしましょう。(僕は既に対応済みだった)

続いて、下記からVercelアカウントを作成して、GitHubアカウントと接続します。

Sign Up – Vercel

無料の「Hobby」プランを選択

好きな名前を設定したのち、「Continue」ボタンを押下

GitHubを選択

続いて、ビルドしていきます。

下のような画面になっているはずなので、先ほど作ったレポジトリの「import」ボタンを押下します。

名前を設定の後、「deploy」ボタンを押下します。

しばらくして、下の画像のような画面に遷移すればデプロイが完了です。

続いて、データベースをセットアップしていきます。

「Continue to Dashboard」をクリックします。

「Storage」タブを選択します。

「Postgress」の「create」ボタンをクリックします。

規約を読んで、問題なければ「Accept」ボタンをクリック。

「Database Name」はわかりやすい名前に、「Region」は「Washington, D.C., USA – (iad1) 」に設定ののち、「Create」ボタンをクリック。

「Connect」ボタンをクリック。

「.env.local」タブに移動する。

「Show secret」をクリックした後、「Copy Snippet」をクリックする。

エディター(僕はVSCodeを使っています)に戻り、「.env.example」ファイルの名前を「.env」に変更をするとの手順公式サイトに書いてあったのですが、おそらく、Cahpter1でprismaをインストールした際に、「.env」ファイルが作成されたと思われる(定かではない)ので既にある「.env」ファイルを削除し、「.env.example」ファイルの名前を「.env」に変更をしてください。

そして、上記「Copy Snippet」でコピーしてある情報を.envファイルにペーストしてください。(元々環境変数が書かれていると思うので(「=」までで値は書いていない)、それは削除してください)

※Next.jsのプロジェクトを作成した段階で設定されてると思いますが、「.gitignore」ファイルで「.env」ファイルがGitの履歴から除外されるように設定してあるか確認してください。(.envの情報は機密性が高いもの)

最後に、ターミナルで「npm i @vercel/postgres」を実行して Vercel Postgres SDK をインストールします。

データベースの初期化(Seed your database)

上記までで、データベースが作成できたので、続いてデータベースの初期化をしていきます。

「/scripts/seed.js」ファイルには、請求書テーブル、顧客テーブル、ユーザーテーブル、収益テーブルの作成と初期化するコードが書かれています。(ここではコードの動作は完全に理解できなくてもOKです)

下記のように「package.json」にスクリプトを追加しましょう。(seed.jsを実行するコマンドです)

/package.json
"scripts": {
  "build": "next build",
  "dev": "next dev",
  "start": "next start",
  "seed": "node -r dotenv/config ./scripts/seed.js"
},

では、ターミナルで「npm run seed」を実行してください。

上記のようになればOKです。

データベースを見てみる(Exploring your database)

Vercelの画面からデータベースがどうなっているか確認しましょう。

「Storage」タブを選択。

「Data」を見ていきます。

「Chooe a table」とあるセレクトボックス(?)をクリックすると、テーブル名が表示されるのでどれかを選択しましょう。(ここでは「users」を選択)

usersテーブルにデータが入っていますが、これは「/app/lib/placeholder-data.js」に書いてあるデータと一致します。(seed.jsでplaceholder-data.jsを使ってデータの初期化をしていた)

開発環境でseed.jsを実行したのに、VercelのDBのテーブルにデータが反映されているのがちょっと腑に落ちていないのだけれど、まぁ一旦進めます!(^^;;

クエリを実行(Executing queries)

「Query」タブに切り替えばデータベースを操作できます。

例えば下記SQL文を実行してみましょう。

SQL
SELECT invoices.amount, customers.name
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
WHERE invoices.amount = 666;

成功すれば、下記のような表示になるはずです。

※例えば「DROP TABLE customers」を実行すれば、「customers」テーブルがすべてのデータとともに削除されるため、注意してください。

Chapter 7(Fetching Data)

Chapter7で行っていくことの概略は下記のとおりです。

Now that you’ve created and seeded your database, let’s discuss the different ways you can fetch data for your application, and choose the most appropriate one for the dashboard overview page.

(データベースの作成と初期化が完了したので、アプリケーションのデータをフェッチ(取得)するさまざまな方法について説明し、ダッシュボードの概要ページに最も適切な方法を選択しましょう。)

Learn Next.js: Fetching Data | Next.js(https://nextjs.org/learn/dashboard-app/fetching-data)

データの取得方法の選択(Choosing how to fetch data)

API layer
APIs are an intermediary layer between your application code and database. There are a few cases where you might use an API:

If you’re using 3rd party services that provide an API.
If you’re fetching data from the client, you want to have an API layer that runs on the server to avoid exposing your database secrets to the client.
In Next.js, you can create API endpoints using Route Handlers.

Database queries
When you’re creating a full-stack application, you’ll also need to write logic to interact with your database. For relational databases like Postgres, you can do this with SQL, or an ORM like Prisma.

There are a few cases where you have to write database queries:

When creating your API endpoints, you need to write logic to interact with your database.
If you are using React Server Components (fetching data on the server), you can skip the API layer, and query your database directly without risking exposing your database secrets to the client.

API層

API は、アプリケーションコードとデータベースの間の中間層です。APIを使用するケースがいくつかあります。

  • APIを提供するサードパーティサービスを使用している場合。
  • クライアントからデータを取得する場合は、データベースの秘密がクライアントに公開されるのを避けるために、サーバー上で実行されるAPIレイヤーが必要になります。


Next.jsでは、ルートハンドラーを使用してAPIエンドポイントを作成できます。

データベースクエリ
フルスタックアプリケーションを作成する場合は、データベースと対話するロジックも作成する必要があります。Postgresなどのリレーショナルデータベースの場合は、SQLまたは PrismaなどのORMを使用してこれを行うことができます。

データベースクエリを作成する必要がある場合がいくつかあります。

  • APIエンドポイントを作成するときは、データベースと対話するロジックを作成する必要があります。
  • React Server Components (サーバー上のデータを取得) を使用している場合は、API レイヤーをスキップして、データベースの秘密をクライアントに公開する危険を冒さずにデータベースに直接クエリを実行できます。

Learn Next.js: Fetching Data | Next.js(https://nextjs.org/learn/dashboard-app/fetching-data)

上記部分は、Next.jsやReactに限らず、一般的なデータの取得方法を説明していると思われます。

そしてここでは、「async React Server Components」という方法でデータ取得をやっていきます。

サーバーコンポーネントを使用してデータ取得(Using Server Components to fetch data)

Next.jsではデフォルトで「React Server Components」を使用できます。

「React Server Components」を使用するメリットは下記です。

  1. APIからのデータフェッチなどの非同期な処理をシンプルに描けるようになる(useEffectやuseStateを使わずにasync/await構文を使える)
  2. サーバー上で実行されるため、そこで処理を書けば追加でAPIなどを作らずして、データベースへの操作が直接(Next.jsで)できる。

上記のようなメリットが書かれていました。(3つ書かれていたが、いまいち理解できなかった)

Reactを使って個人開発を行っていたので、なんとなく理解できることでしたが、より詳しく知りたい方は、下記の記事が個人的に理解しやすかったのでご覧ください。

React Server Componentsについて調べた

SQLの使用(Using SQL)

ここでは、Vercel Postgres SDKとSQLを使ってデータベースのクエリを作成していきます。(用意されているから、SQLを使ったことがなくても問題なく進めることができます)

「/app/lib/data.ts」をみると、「@vercel/postgres」から「sql」をインポートしているのがかるかと思います。それを使用することで、データベースにクエリを実行できます。

/app/llib/data.ts
import { sql } from '@vercel/postgres';
 
// ...

上記sqlは任意のサーバーコンポーネントで使えるが、ここではすべてのデータクエリをdata.tsに書いてあります。そしてそれらデータクエリを使いたいところでインポートするようにします。

ダッシュボード概要ページのデータ取得(Fetching data for the dashboard overview page)

では、ダッシュボードページのデータを取得していきます。

「/app/dashboard/page.tsx」に下記のコードをコピペしてください。

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
 
export default async function Page() {
  return (
    <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">
        {/* <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>
      <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
        {/* <RevenueChart revenue={revenue}  /> */}
        {/* <LatestInvoices latestInvoices={latestInvoices} /> */}
      </div>
    </main>
  );
}

上記のコードは下記のようになります。

  • 非同期のコンポーネントになり(「function」の前に「async」をつけた)、await を使用してデータをフェッチできるようになります。
  • データを受け取る、<Card>、<RevenueChart>、<LatestInvoices>という3つのコンポーネントがあり、エラーを防ぐため一旦コメントアウトしています。(この後の作業で修正していく)

<RevenueChart/>コンポーネントのデータ取得(Fetching data for <RevenueChart/>)

<RevenueChart/>コンポーネントのデータを取得するには、下記のように、「/app/lib/data.ts」のfetchRevenue関数を「/app/dashboard/page.tsx」でインポートして、コンポーネント内で使います。

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  // ...
}

そして、「/app/dashboard/page.tsx」の<RevenueChart/>コンポーネントのコメントと、「/app/ui/dashboard/revenue-chart.tsx」のコメント(「// NOTE: comment in this code when you get to this point in the course」の下のコードのコメント)を解除して、「http://localhost:3000/dashboard」を見てみましょう。

下の画像のようになっていればOKです。

<LatestInvoices/>コンポーネントのデータ取得(Fetching data for <LatestInvoices/>)

<LatestInvoices/>コンポーネントは、日付順に並べ替えられた最新の5件の請求書(のデータ)を取得する必要があります。

JavaScriptを使用すれば、全ての請求書データを取得して並べ替えることができます。

データが小さいうちは上記で問題ないですが、アプリが大きくなっていくにつれて、データの転送量やコードの量も多くなってしまい、パフォーマンスやコードの保守性が悪くなっていきます。

上記の方法の代わりに、SQLクエリで最新の5件の情報のみを取得してくる方法があります。(こちらの方がアプリが大きくなった時にパフォーマンスが良くなる可能性があるからこちらをSQLクエリで絞る方法をここでは採用していくということだと思う)

例えば「/app/lib/data.ts」の下記がそのクエリです。

/app/llib/data.ts
// Fetch the last 5 invoices, sorted by date
const data = await sql<LatestInvoiceRaw>`
  SELECT invoices.amount, customers.name, customers.image_url, customers.email
  FROM invoices
  JOIN customers ON invoices.customer_id = customers.id
  ORDER BY invoices.date DESC
  LIMIT 5`;

上記のクエリを使ってデータを取得している「/app/lib/data.ts」のfetchLatestInvoices関数を「/app/dashboard/page.tsx」でインポートします。

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue, fetchLatestInvoices } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  // ...
}

そして、「/app/dashboard/page.tsx」の<LatestInvoices/>コンポーネントのコメントと、「/app/ui/dashboard/latest-invoices.tsx」のコメント(「// NOTE: comment in this code when you get to this point in the course」の下のコードのコメント)を解除して、「http://localhost:3000/dashboard」を見てみましょう。

下の画像のようになっていればOKです。

上記のように「Latest Invoices」に5件のデータが取得できるているかと思います。

試しにVercelのQueryで、先ほど紹介した、「/app/lib/data.ts」のfetchLatestInvoices関数のクエリを実行してみると下の画像の通り、データを5件取得してくることができて、その5件のデータの「Amount」、「Name」、「Image_url」、「Email」を使用して画面を表示しているのがわかります。

練習問題:<Card>コンポーネントのデータ取得(Practice: Fetch data for the <Card> components)

上記2つのコンポーネントのデータ取得と同じようにして、4つの<Card/>コンポーネントのデータ取得をしてダッシュボード概要画面に表示させるようにしましょう。

下記が答えのコードになります。

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import {
  fetchRevenue,
  fetchLatestInvoices,
  fetchCardData,
} from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  const {
    numberOfInvoices,
    numberOfCustomers,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
 
  return (
    <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">
        <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>
      <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
        <RevenueChart revenue={revenue} />
        <LatestInvoices latestInvoices={latestInvoices} />
      </div>
    </main>
  );
}

「http://localhost:3000/dashboard」を見て、下の画像のようになっていればOKです。

ただ、下記の問題があります。

  1. データリクエストは意図せずに相互にブロックし、リクエストのウォーターフォールを作成します。
  2. デフォルトでは、Next.js はパフォーマンスを向上させるためにルートを事前レンダリングします。これは静的レンダリングと呼ばれるもので、データが変更されても、ダッシュボードには反映されません。

この章では①について説明し、次の章(Chapter8)で②について詳しく説明します。

リクエストウォーターフォールとは?(What are request waterfalls?)

A “waterfall” refers to a sequence of network requests that depend on the completion of previous requests. In the case of data fetching, each request can only begin once the previous request has returned data.

(「ウォーターフォール」とは、前のリクエストの完了に依存する一連のネットワークリクエストを指します。 データフェッチの場合、各リクエストは、前のリクエストがデータを返した後にのみ開始できます。)

Learn Next.js: Fetching Data | Next.js(https://nextjs.org/learn/dashboard-app/fetching-data)

サイトに載っている画像の左側、「Sequential」の方のことを言っていると思われます。

つまり、「複数のコンポーネントでデータを取得してくる場合、1つずつデータの取得をしてくるため、その分時間がかかって問題だよね〜」的なことを言っていると思います!(右側は同時にデータ取得をしているため、今回のダッシュボード概要画面では右側が理想形(?))

/app/dashboard/page.tsx
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // ここの行の実行はfetchRevenue()が完了するまで待つ
const {
  numberOfInvoices,
  numberOfCustomers,
  totalPaidInvoices,
  totalPendingInvoices,
} = await fetchCardData(); // ここの行の実行はfetchLatestInvoices()が完了するまで待つ

「ウォーターフォール」が必ずしも悪いわけではないです。

例えば、最初にユーザーのIDとプロファイル情報を取得したい場合。IDを取得したら、友達のリストを取得します。

この場合、各リクエストは前のリクエストから返されたデータに依存するので、「ウォーターフォール」である必要があります。

※先述した通り、このダッシュボード概要画面では「ウォーターフォール」じゃないほうがパフォーマンスが良くなりそうなので、「ウォーターフォール」が良くない例。

並列データ取得(Parallel data fetching)

ウォーターフォールを回避する一般的な方法は、すべてのデータ取得を同時に、つまり並行して開始することです。

JavaScriptでは、「Promise.all()」関数、または「Promise.allSettled()」関数を使用して、すべての「Promise」を同時に開始できます。

例えば、「/app/lib/data.ts」では、下記のように「fetchCardData()」関数で「Promise.all()」を使用しています。

/app/llib/data.ts
export async function fetchCardData() {
  try {
    const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
    const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
    const invoiceStatusPromise = sql`SELECT
         SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
         SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
         FROM invoices`;
 
    const data = await Promise.all([
      invoiceCountPromise,
      customerCountPromise,
      invoiceStatusPromise,
    ]);
    // ...
  }
}

上記のようにすると下記のことができます。

  • すべてのデータ取得の実行を同時に開始すると、パフォーマンスの向上につながる可能性があります。
  • 素のJavaScriptパターンなので任意のライブラリまたはフレームワーク(例えば、ReactとかNext.jsなど)に適用できます。

ただし、このJavaScriptパターンのみに依存することには欠点が1つあります。

1つのデータリクエストが他のすべてのデータリクエストよりも遅い場合はどうなるでしょうか?

※Chapter7は上記のように課題定義で終わっています。

Chapter 8(Static and Dynamic Rendering)

Cahpter7で現状2つの問題があるという話をしました。

  1. データリクエストは意図せずに相互にブロックし、リクエストのウォーターフォールを作成します。
  2. デフォルトでは、Next.js はパフォーマンスを向上させるためにルートを事前レンダリングします。これは静的レンダリングと呼ばれるもので、データが変更されても、ダッシュボードには反映されません。

続いて、②についてみていきます。

静的レンダリングとは?(What is Static Rendering?)

With static rendering, data fetching and rendering happens on the server at build time (when you deploy) or during revalidation. The result can then be distributed and cached in a Content Delivery Network (CDN).

(静的レンダリングでは、データのフェッチとレンダリングがビルド時 (デプロイ時) または再検証中にサーバー上で行われます。 結果は、コンテンツ配信ネットワーク(CDN)に配信およびキャッシュできます。)

Learn Next.js: Static and Dynamic Rendering | Next.js(https://nextjs.org/learn/dashboard-app/static-and-dynamic-rendering)

つまり、ビルド時にサーバーに静的ファイルを作って置いておいてくれるというイメージで良いと思います!(だから動的なデータが反映されない)

こちらも参考にさせていただきました。(「[Next.js]App Router時代の静的サイトの作り方」)

そして、例えば「/app/page.tsx」は「/」、「/app/dashboard/page.tsx」は「/dashboard」でアクセスした時のページになります。

ユーザーがアプリケーションにアクセスするたびに、キャッシュされた結果が表示されます。そして、下記のメリットがあります。

  • Webサイトの高速化 – 事前にレンダリングされたコンテンツ(ファイル)をキャッシュし、グローバルに配布できます。これにより、世界中のユーザーがより迅速かつ確実にWebサイトのコンテンツにアクセスできるようになります。
  • サーバー負荷の軽減 – コンテンツがキャッシュされるため、サーバーはユーザーのリクエストごとにコンテンツを動的に生成する必要がありません。
  • SEO – 事前にレンダリングされたコンテンツは、ページの読み込み時にすでに利用可能であるため、検索エンジンのクローラーにとってインデックス付けが容易です。これにより、検索エンジンのランキングが向上する可能性があります。

上記より、静的レンダリングは、データのないUI、または静的なブログ投稿や製品ページなど、ユーザー間で共有されるデータのUIに役立ちます。定期的に更新されるパーソナライズされたデータを含むダッシュボードには適さない可能性があります。(今回のアプリケーションには適さない)

動的レンダリングとは?(What is Dynamic Rendering?)

静的レンダリングの反対のもは「動的レンダリング(Dynamic Rendering)」です。

動的レンダリングでは、リクエスト時 (ユーザーがページにアクセスしたとき) に、各ユーザーのコンテンツがサーバー上でレンダリングされます。

動的レンダリングには下記のメリットがあります。

  • リアルタイムデータ – 動的レンダリングにより、アプリケーションはリアルタイムまたは頻繁に更新されるデータを表示できます。これは、データが頻繁に変更されるアプリケーションに最適です。
  • ユーザー固有のコンテンツ – ダッシュボードやユーザープロファイルなどのユーザー独自のコンテンツを提供し、ユーザーインタラクション(ウェブサイトやアプリケーションの利用者(ユーザー)がそれらのプラットフォームで行う行動全般)に基づいてデータを更新することが簡単になります。
  • リクエスト時の情報 – 動的レンダリングを使用すると、CookieやURL検索パラメータなど、リクエスト時にのみ知ることができる情報にアクセスできます。

ダッシュボードを動的にする(Making the dashboard dynamic)

By default, @vercel/postgres doesn’t set its own caching semantics. This allows the framework to set its own static and dynamic behavior.

(デフォルトでは、@vercel/postgres は独自のキャッシュセマンティクスを設定しません。 これにより、フレームワークは独自の静的および動的動作を設定できるようになります。)

Learn Next.js: Static and Dynamic Rendering | Next.js(https://nextjs.org/learn/dashboard-app/static-and-dynamic-rendering)

上記は「『@vercle/postgres』ではキャッシュするかどうかの設定がデフォルトではされていない」という理解で良さそうな気がします。

そして、静的レンダリングを無効化(?)するために、サーバーコンポーネントかデータをフェッチ(取得)する関数内で、Next.jsの「unstable_noStore」というAPIが使えます。

下記のように、「/app/lib/data.ts」で「next/cache」からunstable_noStoreをインポートして、データフェッチする関数の先頭に呼び出します。

/app/llib/data.ts
// ...
import { unstable_noStore as noStore } from 'next/cache';
 
export async function fetchRevenue() {
  // Add noStore() here to prevent the response from being cached.
  // This is equivalent to in fetch(..., {cache: 'no-store'}).
  noStore();
 
  // ...
}
 
export async function fetchLatestInvoices() {
  noStore();
  // ...
}
 
export async function fetchCardData() {
  noStore();
  // ...
}
 
export async function fetchFilteredInvoices(
  query: string,
  currentPage: number,
) {
  noStore();
  // ...
}
 
export async function fetchInvoicesPages(query: string) {
  noStore();
  // ...
}
 
export async function fetchFilteredCustomers(query: string) {
  noStore();
  // ...
}
 
export async function fetchInvoiceById(query: string) {
  noStore();
  // ...
}

※unstable_noStoreは実験的なAPIであるため、変更されてしまう可能性があるそうです。安定したAPIを利用したい場合は、「export const dynamic = “force-dynamic”」を使えば良さそうです。(参考「File Conventions: Route Segment Config | Next.js」)

遅いデータフェッチのシミュレーション(Simulating a Slow Data Fetch)

ダッシュボード概要ページを動的にすることができました。

しかし、Chapter7の最後で説明した問題点が残っております。

1つのデータリクエストが他のすべてのデータリクエストよりも遅い場合はどうなるでしょうか?

上記を確認するために、「/app/lib/data.ts」を編集します。

下記のようにconsole.logのコメントを解除し、fetchRevenue() 内のsetTimeoutを指定します。

/app/llib/data.ts
export async function fetchRevenue() {
  try {
    // We artificially delay a response for demo purposes.
    // Don't do this in a real application
    console.log('Fetching revenue data...');
    await new Promise((resolve) => setTimeout(resolve, 3000));
 
    const data = await sql<Revenue>`SELECT * FROM revenue`;
 
    console.log('Data fetch complete after 3 seconds.');
 
    return data.rows;
  } catch (error) {
    console.error('Database Error:', error);
    throw new Error('Failed to fetch revenue data.');
  }
}

「http://localhost:3000/dashboard/」を開くと、読み込みに時間がかかるのがわかるかと思います。また、ターミナルに下記メッセージが表示されていると思います!

Terminal
Fetching revenue data...
Data fetch completed after 3 seconds.

ここでは、遅いデータフェッチをシミュレートするために人為的に3秒の遅延を追加しています。その結果、データがフェッチされている間、ページ全体がブロックされることになります。

つまり、動的レンダリングを使用すると、アプリケーションは最も遅いデータフェッチと同じ速度でしか動作しません。(次からのチャプターで解決していきそうなことですね)

終わりに

Chapter5〜Chapter8までを見てきました!

次の記事ではChapter9〜Chapter12をまとめる予定です。

しばしお待ちを٩( ‘ω’ )و

Next.jsに入門してみた(Chapter5~8)【Next.js, React】
Next.jsに入門してみた(Chapter9〜12)【Next.js, React】こちらの記事ではNext.jsに入門するために、公式サイトのチュートリアルを翻訳し、僕自身の解釈や理解をまとめております。 Reactの基礎を学び終えて、Next.jsに入門したい方はぜひご覧ください。...

Reactを体系的に学ぶならこちらの動画と書籍がおすすめです!
(僕も使いました)

動画

【2023年最新】React(v18)完全入門ガイド|Hooks、Next.js、Redux、TypeScript icon

書籍