旧ブログ(ISSEN)から移行しました

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

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

西原月熙
西原月熙10分で読めます

はじめに

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

対象読者

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

App Router化について

メリット

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

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

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

Next.js Docs | Next.js

Welcome to the Next.js Documentation.

nextjs.org

Pages Routerからの移行は簡単?

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

App Router化する前に

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

Next.jsは、様々なスタイリングの方法があります!
(S)CSS Modules, styled 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の内容をそのままゴソッと移植するだけになります!

src/pages/_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>
    )
  }
}
src/app/_app.tsx
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が上記のような場合、

src/app/layout.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を記載します。

src/app/layout.tsx
"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になると思うので気をつけてみてください。

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

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

pages/配下をすべて削除

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

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への移行方法のざっくりとした進め方を説明しました!

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

Migrating: App Router | Next.js

Learn how to upgrade your existing Next.js application from the Pages Router to the App Router.

nextjs.org

この記事を書いた人

西原月熙
西原月熙

TOKOSのテックリード。上流工程からコーディング、インフラ系まで色々やっている器用貧乏です。最近は特にフロントエンドのキャッチアップに力を入れています💪 好きな音楽はボーカロイドです🤖