Next.js v12 から v13 with app dir に移行する
手持ちのサイトのなかで最もシンプルといった理由から移行してみました。
Upgrade Guide | Next.js を読んだ感じだと、クライアントサイドで動く領域でエラーに遭遇するのだろうと思いました。
要約
🏗️ The app directory is currently in beta and we do not recommend using it in production.
Next.js beta docs にあるように app/ はまだ production 環境下で使うのはツラかったので、後回しにしました。
移行前の環境
- Next.js v12
- TypeScript
- マークダウン処理:hashicorp/next-mdx-remote
- CSS: TailwindCSS
- 検索:Algolia/react-instantsearch/packages/react-instantsearch-hooks-web
移行前の主なディレクトリ構成
src/types/,utils/,hooks/,components/,styles/
pages/:基本的に view だけ_app.tsx,_document.tsxindex.tsx:投稿一覧404.tsxfeed.xml.tsx,sitempa.xml.tsxentry/[...slug].tsx:投稿詳細 e.g. ‘/2022/20221114-next13-upgrade’
docs/:マークダウンコンテンツ- 2022
20221114-next13-upgrade.md
- 2022
public/
やったこと
Next.js や ESLint など各種パッケージのアップデートはガイド通りなので省略する。
appdirの有効化と各種 .config など設定ファイルの修正
※app/ はまだベータ機能なので注意(2022.11.19 時点)
/** @type {import('next').NextConfig} */
const withBundleAnalyzer = process.env.ANALYZE === 'true' ? require('@next/bundle-analyzer')({ enabled: true }) : (config) => config;
const defaultConfig = { experimental: { appDir: true }, // ~}
module.exports = withBundleAnalyzer(defaultConfig){ "scripts": { "lint:prettier": "prettier --check {src,app,pages}/**/*.{js,jsx,ts,tsx}", }}/** @type {import('tailwindcss').Config} */module.exports = { content: [ "src/**/*.{js,ts,jsx,tsx}", "app/**/*.{js,ts,jsx,tsx}", "pages/**/*.{js,ts,jsx,tsx}" ], theme: { extend: {}, }, plugins: [require('@tailwindcss/typography'),],}app dir下に {layout,head,page}.tsx を追加
各種設定の変更を終えたのちに、/app/{layout,head,page}.tsx を作成してみる。なお、v12 で pages/index.tsx に相当するものは、app/page.tsx になった。
export default function Page() { return <div>app/page.tsx</div>}npm run dev

また、pages/hoge.tsx も共存できている。

TailwindCSS の globals.scss を /page/layout.tsx で import すると
import "../src/styles/globals.scss"
export default function RootLayout({ children }) { return ( <html lang="ja"> <body>{children}</body> </html> );}
Data fetching と Static Genrateの修正
Next.js APIs such as getServerSideProps, getStaticProps, and getInitialProps are not supported in the new app directory.
v13 からは generateStaticParams や async function getData()(任意の関数名)が使われるようになった。
投稿一覧ページ
import { getPostsData } from "@src/utils/markdown/getContentData"
async function getData() { const { posts } = await getPostsData(); return posts;}
export default async function Page() { const posts = await getData();
return ( <> <div className="text-red-500">app/page.tsx</div> <pre> {JSON.stringify(posts, null, 2)} </pre> </> )}
投稿詳細ページ
v12 までの投稿詳細ページでは dynamic routes の Catch all routes を利用し、/pages/entry/[...slug].tsx となっていた。v13 からの app/ 下では /pages/entry/[...slug]/page.tsx となる。
export default function Page({ params, searchParams }) { return ( <> <p>{JSON.stringify(params.slug, null, 2)}</p> <p>{JSON.stringify(searchParams, null, 2)}</p> </> );}v13 app/ 環境下での TypeScript が公式で実装途中なために自分で型定義しないといけないと言うこと以外は、基本的に v12 以前と同じ感じ。

import { getPostsData } from "@src/utils/markdown/getContentData";
export async function generateStaticParams() { const { posts } = await getPostsData(); const params = posts.map(({ fileName }) => { return { slug: fileName.split("/") } }) return params}
async function getData(params: any) { const fileName = params.slug.join("/"); const { posts } = await getPostsData(); const post = posts.find((post) => post.fileName === fileName); return post}
export default async function Page({ params, searchParams }) { const post = await getData(params) return <p>{JSON.stringify(post, null, 2)}</p>}
3rd party パッケージを適切にラッピングする
今回の Next.js v13 から useEffect や useState などクライアントで動く ClientComponents (CC) では use client と記載するようになった。一方で記載されてないものはデフォルトでサーバーサイドで動くようになった。ただ、Next.js からは各種パッケージが client で動くかを判別できないので、必要に応じて CC としてラッピングする必要がある。
また、SC から CC に渡せる props にも制限があり、例えば関数 Function や Date オブジェクトなどシリアライズできないモノは直接渡せない。
- 参照
next-mdx-remote
next-mdx-remote は docs/から mdx?ファイルを読み込んで処理するのに利用している。useEffect などが内部で使われており、use client で包む必要がある。
処理した md コンテンツを表示するために用いる <MDXRemote /> に渡すものは主に 2 つあり、型は下の様になっている。components の方は先述した SC から CC に渡せないものなので、components を含めた形でラッパーを作る必要がある。
type Props = { compliedSource: string; components: Record<string, (props: any) => JSX.Element>}なので
"use client";
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote'import { MDXComponents } from './mdx-components';
type Props = Pick<MDXRemoteSerializeResult, "compiledSource">;
export const NextMDXRemote: React.FC<Props> = ({ compiledSource }) => ( <MDXRemote components={MDXComponents} compiledSource={compiledSource} />)これで無事動くようになった。
その他
next/head の <Head /> が無くなり、代わりに head.tsx で指定するようになったのだが挙動が怪しい。production 環境用にはちょっとまだ時期尚早かな感あった。