【Next.js】Pages RouterからApp Routerへの移行手順

Next.js

はじめに

こんにちは、株式会社TOKOSのツキヤです!
今回はNext.jsの最新機能である「App Router」への「Pages Router」から移行する方法について簡単に説明します!
これからApp Routerに移行するという方もまだまだたくさん居るかと思いますので、ぜひ参考にしてください💪

対象読者

  • Pages RouterからApp Routerに移行をしたい方
  • App RouterのサーバーコンポーネントやLayoutについてある程度理解している方

この記事では、App Routerについての解説はあまり記載していません。

概要については こちらの記事 が分かりやすいので、App Routerについて知りたい方は参考にしてみてください。

App Router化について

メリット

個人的なメリットは、layoutをはじめとした、命名規則等のルールに沿うだけで様々な恩恵を享受できることです!

Pages Routerの際は、レイアウトを出し分けるだけでも一苦労だったのが、App Routerの場合は layout.tsx を配置するだけで良くなります✨
また、Suspenseでロード中の画面を表示したい時も loading.tsx を配置するだけです✨

他にはもちろん、サーバーコンポーネントにすることで、最終的な転送量が減るといったものや、Server Actionを用いてサーバー側の関数をシームレスに記述できるといったメリットがあります!
書き出すとキリが無いので、詳しくは公式のドキュメントをご確認ください。

Pages Routerからの移行は簡単?

結論、そこまで難しくは無いです!
pages/app/ は共存することができるので、ページ単位で逐次的に移行を進めることができます💪
そして、その手順の概要を今回説明します😎

App Router化する前に

先に1つだけ確認・変更しておいた方が良いことがあります!
それは スタイリングの方法です!

Next.jsは、様々なスタイリングの方法があります!
(S)CSS Modules, sytled jsx, styled-components, emotion, Tailwind CSS等から、UI系のChakra UI, MUI, Ant design, Mantine, Panda CSS, Kuma UI などなど、かなり多岐に渡ります🙄
これらの中で、「サーバーコンポーネントに対応していない」ソリューションがあります😔
なので、もしそのソリューションを使っている場合は他のソリューションに変更する必要があります💦

「サーバーコンポーネントに対応」しているかをどのように調べるかは、”ゼロランタイム”かどうかを調べればOKです!
“ゼロランタイム” とは、超カンタンに説明するとスタイルをJS等で動的に変更させないことです。コンパイル時にスタイル(クラス)が確定している感じです! (もちろん、 三項演算子等でclassを出し分けることはできます👍)
また、「〇〇 App Router」等でググれば出てくるはずです!

具体的には、Tailwind CSSやPanda CSS, vanilla-extract等はゼロランタイムなのでApp Routerでも使用することができます💪
詳しくはぜひ調べて見てください!

App Router化する手順

それでは早速移行手順について説明します!
今回はTypeScript・Tailwind CSS・Mantine・Recoilを使用しているので、JavaScriptや他のスタイリングソリューションを使っている方は適宜読み替えてください🙇‍♂️

app/layout.tsx の作成

基本的には、_app.tsx_document.tsx の内容をそのままゴソッと移植するだけになります!

import { ColorSchemeScript } from "@mantine/core"
import Document, { Html, Head, Main, NextScript } from "next/document"

export default class _Document extends Document {
  render() {
    return (
      <Html lang="ja">
        <Head>
          <ColorSchemeScript defaultColorScheme="auto" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}
import "@/styles/globals.css"
import "@mantine/core/styles.css"
import "@mantine/dates/styles.css"

import { MantineProvider, createTheme } from "@mantine/core"
import { RecoilRoot } from "recoil"

const theme = createTheme({
  fontFamily: "Noto Sans JP",
  fontFamilyMonospace: "Noto Sans JP",
  headings: { fontFamily: "Noto Sans JP" },
})

export default function MyApp({ Component, pageProps }) {
  return (
    <>
      <Head>
        <title>サイトタイトル</title>
        <meta name="description" content="テストサイト" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <MantineProvider theme={theme}>
        <RecoilRoot>
          <Component {...pageProps} />
        </RecoilRoot>
      </MantineProvider>
    </>
  )
}

_app.tsxと_document.tsxが上記のような場合、

import "@/styles/globals.css"
import "@mantine/core/styles.css"
import "@mantine/dates/styles.css"

import { MantineProvider, createTheme } from "@mantine/core"

const theme = createTheme({
  fontFamily: "Noto Sans JP",
  fontFamilyMonospace: "Noto Sans JP",
  headings: { fontFamily: "Noto Sans JP" },
})

export const metadata: Metadata = {
  title: "サイトタイトル",
  description: "テストサイト",
}

const RootLayout = ({ children }: { children: React.ReactNode }) => (
  <html lang="ja">
    <head>
      <ColorSchemeScript defaultColorScheme="auto" />
    </head>
    <body className={`${notoSansJp.className}`}>
      <MantineProvider theme={theme}>
        <RecoilProvider>
          {children}
        </RecoilProvider>
      </MantineProvider>
    </body>
  </html>
)
export default RootLayout

上記のように移植するだけです✨
export const metadata については、metaタグに関する書き方がこのような方法になったので変更しています!

ですが、グローバルステートのProviderとなるコンポーネントをそのまま移植するとエラーになってしまいます🧐 (今回で言う <RecoilRoot> )
なぜなら、状態管理ライブラリ系は基本的にクライアントコンポーネントでしか動作しないからです。
とは言え、じゃあ layout.tsx"use client" をつけてしまうと、全てのコンポーネントがクライアントコンポーネントになってしまい、App Routerのメリットが低減されてしまいます。。

ツキヤ
ツキヤ

親コンポーネントに "use client" が付いている場合、子コンポーネントはすべてクライアントコンポーネントになってしまうからだね!

そこで、「Composition」を使います!
ちょっと難しく言いましたが、要は children を使うだけです!
以下に RecoilProvider を記載します。

"use client"

import { FC, ReactNode, memo } from "react"
import { RecoilRoot } from "recoil"

type Props = {
  children: ReactNode
}

export const RecoilProvider: FC<Props> = memo((props) => {
  const { children } = props

  return <RecoilRoot>{children}</RecoilRoot>
})

上記のコンポーネントを作った上で、先程の例のように呼び出してあげればエラーが出ずに表示されるかと思います!

基本的には、"use client" の記述があるコンポーネントから呼び出されるコンポーネントは全てクライアントコンポーネントになります。
ですがこのように、クライアントコンポーネントから children としてサーバーコンポーネントを渡すことで「クライアントコンポーネント > サーバーコンポーネント」という呼び出しが可能になるのです💪

pages/ 配下のファイルを1ページ分移植する

大枠の設定は完了したので各ページの移行に移ります!

小さく変更をしていきたいので、まずは "use client" をページにつけてパスを app/ 配下にずらした上で、ファイル名をApp Routerのルールに則り page.tsx に変えます!

ツキヤ
ツキヤ

localhost:3000/users というURLを実現したい場合は、/app/users/page.tsx というファイルを作成すれば実装できるね!

pages/ 配下と app/ 配下でURLが競合しているとエラーになるので、pages/ 配下の同じファイルを削除後に $ npm run dev をし直すと上手くいくと思います!

その後、まだエラーが出る場合には局所的に変更を加えていきます!
大きな違いとしては、useRouter になるかと思います!
Pages Router の useRouternext/router からimportしていたのですが、App Router の useRouternext/navigation から呼び出す必要があります。
更に、Pages Routerでできていた機能がApp Routerでは4つに別れました。下記で説明します!

useRouter

const router = useRouter()

これは一番想像がつくものです!
router.pushrouter.refresh はこのuseRouterから行います!

usePathname

const pathname = usePathname()

これは、現在のパスを取得したりする時に使います!
例えば、自分が今いるURLが localhost:3000/usersの場合、pathname の中身は "/users" になります!

useParams

const params = useParams()

これは、URL内の動的な値を取得するのに使います!
例えば、自分が今いるURLが localhost:3000/users/111 の場合、params.id とすることで "111" が取得できます!

useSearchParams

const searchParams = useSearchParams()

これは、クエリパラメータを取得するのに使います!
例えば、自分が今いるURLが localhost:3000/users?page=1&limit=100 の場合、searchParams.get("page") とすることで "1" が取得できます!

useRouter についての説明はざっくり以上となります!
他にも細々と変える部分はありますが、大きく変わる箇所はこの useRouter になると思うので気をつけてみてください。

getServerSidePropsや、getStaticPropsについては本記事では解説していません🙇‍♂️

一旦クライアント側でのデータフェッチを許容するか、最初からサーバーコンポーネントでの運用をすることで移植は可能です。ぜひチャレンジしてみてください。

pages/配下のページファイルをすべて移植

1ページ分の移行が完了したら、あとはそれに沿って他のページもガンガン移行するだけです!
ここは気合いを入れて頑張りましょう🤣

pages/配下をすべて削除

全てのページの移植が完了したら、残りの _app.tsx_document.tsx をはじめとした、残りのファイル群を削除します!
最終的に /pages というディレクトリを削除して、一旦の移行は完了になります!!

404や500ページ用のファイルについては、本記事では解説していません🙇‍♂️

気になる方は公式の error.js や not-found.js のページを確認しなら移行をしてみてください。

page.tsxをサーバーコンポーネントに変更する

ただ、このままでは layout.tsx の作成の際にもお話したとおり、page.tsx"use client" が記述されていることで、配下の全てのコンポーネントがクライアントコンポーネントになってしまいます。
ここからは、「各 page.tsx"use client" の記述を外してブラウザでチェック → エラーが出たら対応」を根気強く行っていくことになるかと思います。
また、クライアントコンポーネントで作成する必要がある箇所(ex. onClick がある箇所) を別途で適宜コンポーネント作成して、サーバーコンポーネントの領域を広げるようにしていくと、よりパフォーマンスの向上等が見込めます✨

更にその先へ

ここまででもかなりイカしたApp Routerのアプリケーションになったかと思います!!
更にUX等にこだわりたい方は、loading.tsx を各 page.tsx と同じパスに配置してデータを fetch 中の画面をリッチにしてみるのも良いかもしれません😎

また、API Routesを使用しているプロジェクトの場合は、思い切って Server Action に移行して記述量をグッと減らすようにするのも良いかもしれません。

本記事に記載したこと以外にもまだまだ改善の余地があるので、ぜひご自身で試行錯誤してみてください!
需要があれば、上記等の各変更の詳細な説明の記事も執筆します💪

終わりに

今回は、Pages RouterからApp Routerへの移行方法のざっくりとした進め方を説明しました!

最後にはなりますが、公式のマイグレーションガイドがかなり分かりやすく解説しているので、こちらもぜひチェックしてください🙏