🐥

AppRouterに触れてみよう!

2023/05/17に公開

初めに

今回は、Next.js の v13.4 からstableとなったAppRouterを紹介します。
たくさんの新機能がありますが、本記事では TypeScript を使用して以下の 3 つの機能について紹介します。

  1. ルーティング
  2. データフェッチ
  3. サーバーコンポーネントとクライアントコンポーネント

それでは、順番に説明をしていきます。

ルーティング

これまでのNext.jspagesディレクトリに作成したファイル名やディレクトリ名がそのまま URL のパスになっていましたが、これからはappディレクトリ配下にディレクトリを作りファイル名はpage.tsで書く必要があります。今までindex.tsとしていましたが、index という名前は意味をなさなくなります。
例:app/dashboard/page.tsとした場合、/dashboardの URL パスとなります。

ダイナミックルーティング(user/[id]/pages.ts)は今まで通り使えます。


page.ts以外にも重要な意味を持つファイル名が存在するのでいくつか紹介します。

  1. layout.ts
    画面のレイアウトを作っていて複数の画面で同じ UI を使いたいということがあります。例えば、複数の画面で、ヘッダーとフッターを入れたい。などです。このようなときに各ページでヘッダーとフッターをインポートして使っていては少し面倒です。
app/layout.tsx
export default function RootLayout({children}:{children:React.RectNode}) {
    return (
        <html lang="en">
            <body>
                {children}
            </body>
        </html>
    );
}

app ディレクトリ直下に置かれたlayout.tsルートレイアウトと呼ばれ、必ず置いておく必要があり、ルートレイアウトは全てのページで共有されます。
今までの_app_documentがひとつになった感じです。
また、ルートレイアウトでは html タグと body タグが必須です。

さらに、ディレクトリごとにもlayout.tsを作ることができます。

app/dashboard/layout.tsx
export default function DashboardLayout({children}: {children: React.ReactNode}) {
  return{
    <Header />
    {children}
    <Footer />
    }}

こうすることで dashboard ディレクトリ配下のすべてのpage.tsでヘッダーとフッターが表示されます。

  1. loading.ts
    名前を見てわかる通りローディング中の UI を共有できるものです。
app/dashboard/loading.tsx
export default function Loading() {
  return <LoadingSkeleton />;
}

一番近い場所(同じ階層になければ、親のディレクトリをたどっていく)にあるloading.tsが適用されます。

このほかにもローディング時に使えるSuspenseというものも紹介します。
そもそも、現状の問題点として、ページにアクセスする際、ページの全データを取得し、それを表示していますが、そのデータの中に取得に時間のかかるものがあるとそれを他が待ってしまい、表示に時間がかかってしまいます。
これを解決するために、Suspenseというものを使います。

app/dashboard/page.tsx
import { Suspense } from 'react'import { PostFeed, Weather } from  './Components'export default function Posts() {
  return
   <section>
     <Suspense fallback={<p>Loading feed...</p>}><PostFeed />
     </Suspense>
     <Suspense fallback={<p>Loading weather...</p>}><Weather>
     </Suspense>
   </section>
 );
}

Suspenseで非同期処理を行うコンポーネントをラップします。ラップされたコンポーネントを読み込んでいる間は、fallback に渡した UI を表示します。そして、読み込みが完了したコンポーネントは順次画面に表示されます。
使い道として、ブログの本分とそれについたコメントをそれぞれ、Suspenseでラップし、それぞれが、それぞれを待つことなく読み込めた方から表示することができます。
他の使い方は、React のドキュメントで紹介されています。

  1. error.ts
    これも、名前でわかります。
app/dashboard/error.tsx
'use client';

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  useEffect(() => {
    console.error(error);
  }, [error]);

  return (
    <div>
      <h2>Something went wrong!</h2>
      <button
        onClick={
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  );
}

dashboard配下でエラーが発生した場合、このコンポーネントがlayout.tsでラップされて表示されます
reset 関数を実行することで、このページを再レンダリングすることができます。
1行目の'use client'は必ず書く必要があります。'use client'の意味については後程紹介します。

レイアウトのエラー処理

error.tsでは、同階層にある、layout.tsのエラーを補足することができません。
これを解決するためには、そのlayout.tsの親のディレクトリにerror.tsを置いておく必要があります。

ルートレイアウトのエラー処理

先ほど、layout.tsのエラーを補足するには親のディレクトリにerror.tsを置いておく必要があると紹介しましたが、一番上の階層である、ルートレイアウトはどのようにエラーを補足すればよいのでしょうか。
global-error.tsを使用することで解決できます。

app/global-error.tsx
'use client';

export default function GlobalError({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  );
}
  1. not-found.ts
    not-found.tsは存在しないページにアクセスした際のページの UI を定義できます。
app/not-found.tsx
export default function NotFound() {
  return (
    <>
      <h2>Not Found</h2>
      <p>Could not find requested resource</p>
    </>
  );
}

これも、各ディレクトリに置いておくことができます。

データフェッチ

App Routerでは、fetchAPI が拡張されており、キャッシュに関する設定をすることができます。

静的なデータの取得

静的なデータとは、どのユーザーにも同じ値を返すデータのことです。例えば、ブログの記事などがそれに該当します。

fetch("https://...", { cache: "force-cache" });

force-cacheを設定することで、まずキャッシュをみて一致するものがあれば、それを返します。
ちなみに、cache オプションのデフォルトがforce-cacheなので、書かなくてもよいです。

データの定期的な更新

cache: "force-cache"ではキャッシュをみてあれば、それを返してしまうため、新しいデータがあっても、データが更新されることはありません。
キャッシュされたデータを一定時間ごとに更新するにはrevalidateオプションを使用します。

fetch("https://...", { next: { revalidate: 10 } });

revalidateに設定された、秒数が立つまでは何度リクエストがあっても、キャッシュの更新がされず、revalidateに設定された秒数が立ってから次のリクエストのタイミングでキャッシュが更新されます。Next.js のドキュメント

Note: This is equivalent to Static Site Generation (SSG) and Incremental Static Regeneration (ISR) in the Pages Router.

と書かれています。
今までは SSG,ISR で取得してきたデータは pages ディレクトリからバケツリレーでデータを渡していましたが、これからは、コンポーネントごとに SSG,ISR を使い分けることができるようになりました。

キャッシュを使用しない方法

最後にキャッシュを使用しない方法を紹介します。

fetch("https://...", { cache: "no-store" });

リクエストのたびに、キャッシュを見ずにサーバーからデータの取得をします。

サーバーコンポーネントとクライアントコンポーネント

まずは、サーバーコンポーネントとクライアントコンポーネントとは何かについて説明します。
この二つの違いは、コードが実行される場所です。それぞれ、サーバーとクライアントでコードが実行されます。

AppRouter のコンポーネントはデフォルトで サーバーコンポーネント になっています。
クライアントコンポーネント として使用するには、ファイルの一番上に'use client'と記載する必要があります。
では、どのように使い分けるべきでしょうか。
Next.js のドキュメントの表をもとに紹介します。

サーバーコンポーネントの使いどころ

  • データの取得
  • バックエンドのリソースへのアクセス
  • 機密情報へのアクセス(API キーなど)
  • クライアントサイドの JavaScript を削減する

クライアントコンポーネントの使いどころ

  • イベントリスナー(onClick など)
  • ステートとライフサイクル(useState など)
  • ブラウザ専用の API
  • 上二つに依存するカスタムフック

主にユーザーのアクション(ボタンをクリックする、フォームに入力をする等)に対して動作するものはクライアントコンポーネントにする必要があります。

レンダリングの仕組み

サーバーコンポーネントとクライアントコンポーネントの入れ子にして使う場合の注意点を理解するには、どのようにレンダリングがされているのか知る必要があります。

  1. クライアントにコードを送信する前に、すべてのサーバーコンポーネントをレンダリングします。
  2. クライアントでは、クライアントコンポーネントをレンダリングし、サーバーとクライアントの情報を統合します。
    ここで大事なのが、サーバーで処理をした後にクライアントで処理をするということです。

サーバーコンポーネントとクライアントコンポーネントをネストして使うときの制約

クライアントコンポーネントの中にサーバーコンポーネントをインポートしてはいけません。これをしてしまうと、サーバーコンポーネントをレンダリングした後、クライアントコンポーネントをレンダリングしようとしたときに、再度サーバーコンポーネントをレンダリングする必要が生じます。

app/example-client-component.tsx
'use client';

import ExampleServerComponent from './example-server-component';

export default function ExampleClientComponent({
  children,
}: {
  children: React.ReactNode;
}) {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <ExampleServerComponent />
    </>
  );
}

これは非対応パターンとして Next.js のドキュメント で紹介されています。推奨パターンとしては

app/example-client-component.tsx
'use client';

import { useState } from 'react';

export default function ExampleClientComponent({
  children,
}: {
  children: React.ReactNode;
}) {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      {children}
    </>
  );
}
app/page.tsx
import ExampleClientComponent from './example-client-component';
import ExampleServerComponent from './example-server-component';

export default function Page() {
  return (
    <ExampleClientComponent>
      <ExampleServerComponent />
    </ExampleClientComponent>
  );
}

このようにchildrenとして渡すパターンです。こうすれば、<ExampleServerComponent>を先にレンダリングさせることができます。

まとめ

Next.js v13.4 の AppRouterについて紹介しました。

新しいルーティングシステムでは、より直感的なファイルベースのルーティングを実現し、特別なファイル名を用いることでレイアウトやエラーハンドリングなどの制御を行うことができます。また、Suspense を使用することで、非同期のデータフェッチをスムーズに行うことが可能となりました。

データフェッチにおいては、fetchAPI の拡張により、キャッシュの利用や定期的な更新を簡単に設定できるようになりました。これにより、データの取得と表示をより効率的に行うことができます。

サーバーコンポーネントとクライアントコンポーネントの使い分けにより、パフォーマンスの最適化とユーザー体験の向上を図ることができます。

参考にしたサイト

https://app-dir.vercel.app/
https://nextjs.org/docs

Discussion