【Next.jsの新機能】App Router を早速本番環境で使ってみた

Next.jsの新機能app router。ロゴがかっこいい

こんにちは、アイザックでエンジニアをしている石川です。 アイザックの新規事業では Next.js を使って開発をしています。もともと Pages Router での開発でしたが、App Router が Stable になったタイミングで App Router への移行を行いました。 まだ試せていない&紹介しきれない機能も多いですが、主な変更点などをご紹介します。

そもそもApp Routerとは

Next.jsのv13.4からStableとなったファイルシステムベースのルーターです。

今までのPages Routerとは違い、/pages ではなく /app をトップレベルのディレクトリとして扱ったり、RSCやNested Layoutなどを使用できるようになりました。

他にも様々な機能が追加され、Pages Routerのときより開発体験やパフォーマンスが良くなりました。その他の詳しい機能などは Next.js の公式ドキュメントをご覧ください。

主な変更点

ディレクトリ構成

App Router では Private Folders という機能があり、 _ から始まるディレクトリをルーティング対象から除外できます。

移行してからはこの機能を使い、 page.tsx などと同階層に _components というディレクトリを作り、そこにそのページで使われるコンポーネントを配置、という形にしました。 後ほど紹介する Server Actions のファイルも同じように配置しています。

こうすることによって自然にコロケーション出来るようになり、ファイルの配置場所にも困らなくなりました。

データフェッチ

もともと Pages Router のときは、データフェッチには GraphQL + Apollo Client を使用していました。

App RouterではNext.jsが推奨しているデータフェッチパターンを参考にして、データフェッチをRSCで行うようにしました。これによりクライアントでのキャッシュが不要になったのでApollo Clientをやめ、シンプルな graphql-requestへ変更しました。

キャッシュを効かせたい部分ではReactのcache関数を使用するようにしています。

また、RSC の登場によって以下のようにデータフェッチ部分がより簡潔に書けるようになりました。

(例)

export default async function Page() {
  const user = await fetchUser();

  if (!user) {
    redirect('/');
  }

  const data = await fetchData();

  // 余談ですが、順不同で複数fetchしたい場合は並列で取得することも出来ます
  // const [data1, data2] =
  //   await Promise.all([
  //     fetchData1(),
  //     fetchData2(),
  //   ]);

  return (
    // ...
  )
}

Mutation

更新系の処理は Server Actions を使用しています。 Server Actions は、クライアントサイドでフォームの送信やボタンをクリックしたときに、サーバーサイドで実行される関数を呼び出せる機能です。

この機能は 2023/09/11 現在 experimental な機能のため、今後変更される可能性があります。 この機能を使用するためには、next.config.jsexperimental: { serverActions: true } を追加します。

以下のように actions.ts のような新しいファイルを追加し、ファイルトップで use server というディレクティブを宣言すると、このファイルのすべての関数がサーバーサイドで実行されるものとして扱われます。

(例)

'use server';

const createPost = async (data: Data) => {
  // Prismaを使ったり、sqlを直接書いたりすることができる

  revalidatePath('/posts');
};

返り値にエラーオブジェクトやテキストを設定することでエラーハンドリングをしたり、フォーム(Server Actions の呼び出し側)では useFormStatususeTransition といった hooks を用いた状態管理もできます。

また、next/cacherevalidatePath, revalidateTag といった関数を使うことでデータの再検証を行い、更新後に最新のデータを表示することが出来ます。

UI ライブラリ

RSC が CSS-in-JS に対応していないため、 MUI をやめてゼロランタイムな Tailwind CSSRadix UI に変更しました。

Radix UI とは

Radix UI は ヘッドレス UI と呼ばれる UI ライブラリの一つで、コンポーネントの機能だけを提供してくれます。ダークモード対応やアクセシビリティのための実装などもされています。

今回のプロダクトでは、app/_components/ui 以下に Tailwind CSScva を使って スタイリングしたプリミティブなコンポーネントを集約し、それらを使い回す形で運用しています。

Metadata API

Metadata に関する記述も以前は Head コンポーネントを使用していましたが、App Router では metadata という変数名のオブジェクト を page, もしくは layout から export することで設定できます。

動的なメタデータにしたい場合は generateMetadata という関数を export することが出来ます。この関数では searchParams を受け取ったり、関数内で fetch を行うことが出来ます。

(例)

export const generateMetadata = async ({
  params: { id },
}: Props): Promise<Metadata> => {
  const data = await fetchData(id);

  if (!data) notFound();

  return {
    title: `data: ${data.name}`,
  };
};

また、faviconapple-icon などは決められたファイル名にして特定のディレクトリに配置するだけで、Next.js 側で自動的にそれに対応するタグを追加してくれます。

https://nextjs.org/docs/app/api-reference/file-conventions/metadata/app-icons

Nested Layout

layout.tsx というファイルを作成することで Layout を追加できます。また、ネストさせることも出来るようになりました。

(Route Groups)https://nextjs.org/docs/app/building-your-application/routing/route-groups という機能と組み合わせることによって、より柔軟にレイアウトを定義することが出来ます。

レンダリング方法

Pages Router では、getServerSidePropsgetStaticProps といった関数を使ってページのレンダリング方法を指定していましたが、App Router ではこれらの関数は廃止されています。

かわりに fetch メソッドの 第二引数のオプション等で設定することができます。

const staticData = await fetch(`https://...`, { cache: "force-cache" }); // ビルド時にキャッシュされる。デフォルトの設定

const dynamicData = await fetch(`https://...`, { cache: "no-store" }); // キャッシュせず、リクエスト毎にサーバーから取得

const revalidatedData = await fetch(`https://...`, {
  next: { revalidate: 10 },
}); // 10秒の間キャッシュされる。

また、Next.js の (Dynamic Functions)https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-functions と呼ばれる関数 (ユーザーの cookie やリクエストヘッダー、URL のクエリパラメータなど、リクエスト時にしか取得できない情報を取得する関数)を使用しているルートは、動的なページとしてレンダリングされるようになります。

ルート単位で設定したい場合は、layout や page から特定の変数を export することでも設定できます。

https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config

まとめ

実際に移行してみて、ディレクトリ構成やデータフェッチの部分がよりシンプルになったり、パフォーマンスが改善されたりなど様々なメリットを感じられました。

参考程度に特定のページでApp Router移行前後のチャンクサイズを測ったところ、以下のような結果になりました。

検索ページ App Router Pages Router
chunk size 2.59 kB 4.33 kB
First Load JS 145 kB 251 kB
チケット一覧ページ App Router Pages Router
chunk size 431 B 2.77 kB
First Load JS 91.8 256 kB

UIライブラリやGraphQLクライアントの変更も含まれていますが、チャンクサイズを減らすことが出来ています。

ただ、メリットだけでなく使用技術が RSC に対応していない場合があったり、まだ新しめの技術なためライブラリ側の不具合がある程度存在する、などのデメリットもあると思うので、そのあたりは注意が必要かなと思います。

ざっくりですが、App Router へ移行した際の主な変更点や新しい機能の紹介でした。まだ試せていない機能も多いので、今後も色々試してみたいです。

最後までご覧いただきありがとうございました。