🫢

【Next.js】実務でapp routerに移行した所感

2023/08/02に公開

app router とは

next.js で使用できるファイルシステムベースのルーターです。以前は pages router というものがありましたが、そちらの進化系といえます。
ポイントは error.tsx や loading.tsx などの決められた名前でコンポーネントを作成することで、エラーバウンダリーやサスペンスなどの機能が簡単に利用できるようになったことです。
これにより開発速度が上がり、面倒な実装はフレームワークに任せることができます。
また、内部で RSC を使用しており、これとサスペンスにより、コンポーネントレベルで SSR と CSR を組み合わせられるようになりました。

今回はそういった機能の、弊社プロダクトにおける使用例を紹介します。

使用技術

error.tsx

エラーバウンダリーです。fallback コンポーネントを書くだけで ok です。

import { Button, Text } from "@components/ui";
import { useEffect } from "react";

export const ErrorFallback = ({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) => {
  useEffect(() => {
    console.error("error", error);
    // ここに外部サービスへのエラー送信処理などを書く
  }, [error]);

  return (
    <div className="w-full h-full flex flex-col items-center justify-center gap-4">
      <Text size="strong">エラーが発生しました</Text>
      <Button onClick={reset} label="リロード" />
    </div>
  );
};

layout.tsx

ネスト出来る点が大きく変わりました。これにより、グローバルに使いたい機能は app/layout.tsx に、user 画面で使いたいものは app/user/layout.tsx に書く、などが出来るようになり、非常にわかりやすくなりました。
我々のプロダクトでは、app/[lang]/layout.tsx という国際化対応のためのレイアウトを作っています。

import { DictionaryProvider } from "context/DictionaryContext";
import { ReactNode } from "react";

import { getDictionary } from "./get-dictionary";

const Layout = async ({
  children,
  params,
}: {
  children: ReactNode;
  params: {
    lang: string;
  };
}) => {
  const dict = await getDictionary(params.lang);
  return <DictionaryProvider dictionary={dict}>{children}</DictionaryProvider>;
};

export default Layout;

このように階層ごとに責務を分けています。

route groups

app router ではディレクトリ名の命名方法により様々な機能が使えます。route groups もその内の一つです。
ディレクトリ名を(ディレクトリ名)のように()で囲むことで、そのディレクトリをパスに含めず複数のページをまとめることができます。

例えば以下の構成の場合、https://localhost:3000 にアクセスするとapp/(main)/(dashboard)/page.tsxが表示されます。このページではapp/(main)/layout.tsxapp/(main)/(dashboard)/layout.tsxが入れ子になって page.tsx を囲みます。
我々のプロダクトは大きく 2 つのレイアウトに分かれているため、この機能を使い、app/(main)app/(auth)のように 2 つに分けています。

app
├── (main)
│  ├── (dashboard)
│  │  ├── layout.tsx
│  │  └── page.tsx
│  ├── user
│  │  └── [id]
│  │     └── page.tsx
│  └── layout.tsx
...

private folders

ディレクトリ名を_ディレクトリ名_から始めると、ルーティングから省けます。ルートグループとの違いは、private folders では全ての子セグメントをルーティングから省きます。つまり、_user/page.tsx_user/detail/page.tsx/user/user/detailにアクセスしても 404 となります。他の error.tsx などももちろん使えません。
これが結構便利で、後で説明するコロケーションパターンに使えます。

dynamic routes

定番のuser/[id]とかですね。我々のプロダクトでは id や言語設定などで使っています。

i18n

layout のところで説明したように、上記の dynamic routes を app/[lang]として使うことで、パスによる国際化をしています。/ja/user/[id]とかですね。ドメインではやってません。ドメインで実装すると、params から言語設定を取得することができず柔軟性に欠けるためです。
具体的な実装は省きますが、i18n の対応はフルスクラッチで行いました。app router に対応したいい感じのライブラリがなかったので。
意外と簡単に多言語対応, 動的インポート, 入力補完, ビルド時の検知, 埋め込みのスタイリングなどが実装できたので、手間は少なくてすみました。

route handler

page router における api route ですね。secret を使うような処理をここに書いていますが、server actions が安定したらそちらに移行する可能性が高いです。

middleware

middleware は pages の頃と変わらないので割愛します。nextauth で使用しています。

parallel route

我々のプロダクトでは 1 つのページで使っており、試験運用中といったところです。
error.tsx とかを使えるので、コンポーネントで分ける場合に比べて next.js の恩恵に授かれます。layout.tsx 内でしか使えないので、同じページ内のそれなりに大きなまとまりを分割したいときに使うといいです。
例えば、チャットアプリで左にユーザーリスト、右にチャットルームを表示するようなレイアウトを作るときに使えると思います。

移行してよかったこと

コロケーションパターンでファイルが追いやすくなった

これがめちゃくちゃいいです。page router のときはファイル名が UserDetail.tsx などでもページとして扱われていましたが、app router では page.tsx や route.tsx などの予約された名前のファイルのみがルーティングの対象なので、それ以外のファイル名ならルーティングに影響を与えずに配置することができます。
つまり、あるページでしか使われないようなコンポーネントをそのページの近くに配置することが出来るようになり、エクスプローラーの可読性が上がりました。
現在は private folders も併用して以下のようなイメージで構成しています。

$ exa -T --level=4
user
├── [id]
│  ├── _UserDetail // プライベートフォルダ
│  │  ├── index.ts
│  │  ├── UserDetailContainer.tsx
│  │  └── UserDetailPresenter.tsx
│  ├── page.tsx // ルーティング用。メタ情報もここ。
│  └── UserDetail.tsx // 実際の内容はここに書く
├── _UserTable
│  ├── index.tsx
│  ├── UserTableContainer.tsx
│  └── UserTablePresenter.tsx
├── layout.tsx
├── page.tsx
└── User.tsx

色々試してるのでまだまだ変更されると思います。

移行してよくなかったこと

特にありません。
あえて言うなら機能が膨大になったので next.js への依存が大きくなったことです。が、vercel という巨人の肩に乗るメリットの方が大きいと思うので、あまり気にしていません。

悩み

最大の悩みが graphql のフェッチです。
apollo client を使用していますが、クライアントサイドフェッチだとキャッシュシステムが便利で、id によって自動でキャッシュや画面を更新してくれるので重宝しています。
app router の対応として apollo client 公式は、

更新される値はクライアントサイドフェッチを、更新されないような値はサーバーサイドフェッチを使用するように。

と言っています。ですが、これには厳格なルールがないので、強くマネジメントをしないと途端にカオスになります。
これが大きな懸念点で今のところクライアントサイドフェッチに留まっています。
ただ app router を使いこなすということは RSC を使いこなすということでもあり、サーバーサイドフェッチを使わないとメリットが大きく減ってしまいます。したがって現在は移行を考えつつも待ちの姿勢です。

今後の展望

まだ使ってないものがあるのでそのあたりも有効に使いたいと考えています。turbopack は特に使いたいですね。また、loading.tsx でのスケルトンスクリーンもリッチな見た目にするなら是非使いたいところです。

また、これは先の話ですが、アプリなど他のプラットフォーム向けに react のコンポーネントを使い回せるようにしたいと思っており、そのために turborepo を使えたらいいなと思っております。

まとめ

next.js の app router で弊社が使用している技術などを話しました。何年も続いたフロントエンドの潮流が変わりそうなので、これからも注目していきたいですね。

おまけ

弊社では、フロントエンドのエンジニアを募集しています。最新のApp Routerを使って、新規サービスを一緒に立ち上げて行きましょう!

https://herp.careers/v1/ficilcom/s1mr4YKuspxv

フィシルコム

Discussion