💭

Next.jsのRoutingの基本について

2023/12/19に公開

こんにちは、PharmaXのエンジニアのCaiです。
この記事、PharmaXのアドベントカレンダーの19日目の記事です。

https://qiita.com/advent-calendar/2023/pharma-x

弊社は現在、Next13+とApp Routerの新機能を積極的に取り込むとの方針でフロントエンドのリファクタリングを進めています。
また、保守性の観点から社内では定期的に勉強会を行っています。
そこで、情報発信の観点から新機能の紹介と今我々のプロジェクトにはどういう形で新機能を取り込んでいるのかについて紹介しよう思います。
今回はその第一弾としてRoutingについて紹介していきます。

プロジェクトを作る

実際のコードを触りながら紹介したいので、動作確認用のプロジェクトを作りましょう。
npx create-next-app@latestでプロジェクトを作り、ここはあえてPages Routerで作りましょう。

すると幾つのファイルがすでに初期化されていて、「そういえばPages routerではどんな感じだったっけ?」の人は、まずはこの状態で色々触って感覚を取り戻すと良いでしょう。

Routingを移行する

「Pages RouterからApp Routerへ移行する」の第一歩としてはRoutingの移行です。
最初はsrc直下で /appを作って、ここから作業していきます。
App RouterはPages Routerの時と同じく、いくつか「下準備」となるファイルが必要です。
Pages Routerにおいてそれが/pages/_app.tsxと/pages/_document.tsxでしたが、App Routerでは/app/layout.tsxでした。

Pages Router

App Router

Routingを定義する

Routingの定義に関しては、App RouterもPages Routerもそこまで変わりません。
Pages RouterのRoutingは「/pages をはじめ(root)とし、index.tsx|jsx を定義し、export defaultしたdirectoryをleafとするTree構造」に対して、
App RouterのRoutingは「/app をはじめ(root)とし、page.tsx|jsx を定義し、export defaultしたdirectoryをleafとするTree構造」と理解して差し支えがありません。
なので、/app/home/page.tsxのようなファイルを作成すれば、/homeでアクセスできるということになります。


このようなファイルを作成して、/homeをアクセスしてみるとapp router home pageが表示されます。

これがApp RouterでRoutingを定義する際の基本となります。
/home/subのようなネストされたRoutingや/user/5、/user/12のような動的なRoutingの定義の仕方などは公式ドキュメントを見ながら作ったり動作確認したりすると良いでしょう。
基本ファイルやフォルダのネーミングルールに沿って作成すれば躓くことなくできると思います。
(動的Routingの細かい機能はまた別の機会で紹介しようと思います。)

注意点

では、既存のプロジェクトをApp Routerに移行したい場合は、/appと/app/layout.tsxを作成して、あとはindex.tsxをひたすらpage.tsxにファイル名を変更すればいいのでしょうか?
結論から言うと違います。
App Routerは/appにファイルを置くことが目的ではなく、/appにファイルを置くことで、RSCやServer Actionsといった新機能が使えるようになるのが目的です。
/appにファイルを移動して、新機能をどんどん取り込んでいこうと、個人プロジェクトならまだいけますが、会社のプロジェクトや既に運用されているシステムはなかなかそうはいきません。
なので「Pages RouterからApp Routerへ移行する」第一歩として、「とりあえずファイルを/appに移動しよう」はアリと思います。
今回はそれをやる際の注意点を2つ紹介します。

  • App Router と Pages Routerの共存は可能である一方、Routingは重複を許さない
  • (デフォルト)/appに定義されたComponentはServer Component

App RouterとPages Routerの共存

App Router と Pages Routerの共存は可能です。
これなら一気に移行するのではなく、段階的に行うことができます。
例えば下記のような構造で想像しましょう。

既存のPages Routerでは/homeと/subの画面があります。
ここではまずsub画面をApp Routerに移行したい。
するとやるべきことは/pages/sub/index.tsx を/app/sub/page.tsxに移行すれば移行は完了です。(/pages/sub/index.tsxは消します)
このように↓

一方、弊社ではちょっと変わった事情があります。
「pages routerのsub画面は機能が多く含まれており、小さなリファクタリングを行い、その都度テストするとテストコストがもったいないので、リファクタリングが全部完了までpages routerのsub画面はそのまま残したい。ただ一方、 リファクタリング中の画面も検証環境で見れるようにしたい」
これはつまり言い換えると、「sub画面を同時にpages routerとapp routerに存在させたい」です。
コードで見ると下記のようなことになります。


実はこれはできません。ビルドでエラーが出ます。

Conflicting app and page file was found, please remove the conflicting files to continue: "src/pages/sub/index.tsx" - "src/app/sub/page.tsx"

これが、/subと言うRoutingがpages router にもapp routerにも存在しているから衝突が起きた、ということでした。
なので、弊社は下記の運用で段階的に移行を行っております。

  1. 移行対象画面を/app/app-subのようなRoutingを定義し、リファクタリングを行う
  2. /app/app-subのリファクタリングが完了後、テストを行う
  3. /app/app-subのテストが完了後、/pages/subを削除し、/app/app-subを/app/subに名前を変更して、移行完了とする

Server ComponentとClient Componentの境界線

Server ComponentとClient Componentについては別途紹介したいと思いますので、ここでは触れませんが、いくつ「結論」だけ紹介します。
Server Componentでは、hookなどRuntimeに依存するものや、onClickなどuser interaction依存の処理は書けません。
そして、/app に書かれたComponentは全てデフォルトではServer Componentとして扱われます。
なので、例に出ているsub画面のcomponentが下記のようなコードなら、単純にファイルを移動してファイル名を変えるだけでは移行は完了されません。

このままでは、このようなエラーが出ます。

./src/app/sub/page.tsx
ReactServerComponentsError:

You're importing a component that needs useEffect. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
Learn more: <https://nextjs.org/docs/getting-started/react-essentials>

   ,-[/Users/cai_jiaxin/projects/next-demo/src/app/sub/page.tsx:1:1]
 1 | import { useEffect } from "react"
   :         ^^^^^^^^^
 2 |
 3 | export default function Sub() {
 4 |   useEffect(() => {
   `----

Maybe one of these should be marked as a client entry with "use client":
  ./src/app/sub/page.tsx

useXXXといったものはClient Componentでしか動作しないため、"use client"をこのComponentに書いてくださいとの旨でした。

こういうエラーが出た場合、エラーが出たファイルの先頭に"use client"を付けば解消されます。
このように。

それ、全部やらないとダメ?

ここで「hookやeventを貼っているComponentはかなり多いけど、全部のファイルにこれを付けないとダメなのか?大変だぜ」と思う人もいるかもしれません。
実はそれをやる必要はありません。
"use client" が書かれたComponentは、「これはClient Componentである」の意味ではなく、「ここからはClient Componentである」の意味でした。
下記の例を見ましょう。

Sub Componentには"use client"が書かれている、これは「Sub ComponentからはClient Componentである」と宣言する意味です。
そうなると、Sub ComponentにimportされたA ComponentもClient Component扱いなので、"use client"を書かなくてもhookなどが使えます。
そして、B ComponentはA Componentにimportされているので、当然B Componentも同様に"use client”の記述が省略されます。
このように、上層のComponentに"use client”をつければこのエラーは解消されます。
一旦、Routingだけの移行はこれで完了できます。

注意点:

  • あくまでエラー解消を目的とした場合、上層のComponentに"use client”を付けると言う手法を紹介しましたが、これはアンチパターンです。
  • Server Component / Client Component / use clientについての詳細は別途紹介しようと思います

File Colocation

RoutingにおいてApp RouterとPages Routerではあんまり違いはありませんでした。
私的にApp Routerで嬉しい機能としてはFile Colocationがデフォルトで提供されていることです。
ここでFile Colocationがもたらす利点については深く語りませんが、App RouterとPages RouterでFile Colocationをやろうとするとどう違うかを紹介しようと思います。

Pages Routerではビルドエラーが起きます

/pages/home/index.tsxでしか使わないuseCustomerというHookがあるとしましょう。

/pages/home/index.tsxでしか使わないので、/pages/home/hooksを作成し、その中に置きます。そのまま開発環境で確認すれば、画面上は問題なく、pages home page:testが表示されるはず。
しかし、このままビルドを行うとエラーが出ます。

> Build optimization failed: found page without a React Component as default export in 
pages/home/hooks/useCustomer

See <https://nextjs.org/docs/messages/page-without-valid-component> for more info.

リンクを辿ってみると、「Pages RouterはRoutingにのみ利用され、pageに該当しないもの(export defaultしないもの)はこの中には置けません」との旨でした。

App Routerではどうなる?

同じことをApp Routerでやってみましょう。

/app/sub/hooksにuseCustomerを定義し、それを/app/sub/page.tsxにimportします。
ブラウザで/subにアクセスすると問題なくapp sub page: textが表示されるはず。
次はこのままビルドしてみましょう。

問題なくビルドが成功だと思います!
ここで、「/app/sub/hooksというフォルダが作られるということは、/sub/hooksというRoutingが作られる意味では?」と思う人もいるでしょう。
それを試しに/sub/hooksにアクセルしてみると、ちゃんと404が表示されると思います。
実はこれがApp Routerの原則の一つで、「page.tsx|jsxがexport defaultされていなければ、そのRoutingはアクセスできない」でした。
逆に言うと、「page.tsx|jsxを作ってexport defaultするとそのURLでアクセスできてしまう」ということ。プロジェクトをColocation設計に適用して/appの配下で/componentsや/hooksを作る時にはこのルールを注意してやるといいでしょう。

最後に

弊社ではApp Routerに移行してから、特にFile Colocationの恩恵を受けていると感じました。
リファクタリングを着手する当初は常に「これって消していい?」や「これ触ったらどこまで影響するんでしたっけ?」といった問題が頭の中にありました。
ただし現在はApp RouterのRouting機能を利用し、プロジェクトのdirectory構造だけで大分ファイルごとのスコープがわかりやすくなりました。
今回はRoutingに関して基本的な機能を紹介しましたが、Next13(執筆時点では既に14になりましたがw)ではまだたくさん新機能があり、順番を踏んで少しずつ紹介していきたいと思います。

PharmaXでは、12月に以下のイベントを開催します。
ぜひご参加ください!

https://yojo.connpass.com/event/302642/

PharmaXテックブログ

Discussion