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.

Getting Started | Next.js

Next.js beta docs にあるように app/ はまだ production 環境下で使うのはツラかったので、後回しにしました。

移行前の環境

移行前の主なディレクトリ構成

  • src/
    • types/, utils/, hooks/, components/, styles/
  • pages/:基本的に view だけ
    • _app.tsx, _document.tsx
    • index.tsx:投稿一覧
    • 404.tsx
    • feed.xml.tsx, sitempa.xml.tsx
    • entry/
      • [...slug].tsx:投稿詳細 e.g. ‘/2022/20221114-next13-upgrade’
  • docs/:マークダウンコンテンツ
    • 2022
      • 20221114-next13-upgrade.md
  • public/

やったこと

Next.js や ESLint など各種パッケージのアップデートはガイド通りなので省略する。

appdirの有効化と各種 .config など設定ファイルの修正

app/ はまだベータ機能なので注意(2022.11.19 時点)

next.config.js
1
/** @type {import('next').NextConfig} */
2
3
const withBundleAnalyzer = process.env.ANALYZE === 'true'
4
? require('@next/bundle-analyzer')({ enabled: true })
5
: (config) => config;
6
7
const defaultConfig = {
8
experimental: {
9
appDir: true
10
},
11
// ~
12
}
13
14
module.exports = withBundleAnalyzer(defaultConfig)
package.json
1
{
2
"scripts": {
3
"lint:prettier": "prettier --check {src,app,pages}/**/*.{js,jsx,ts,tsx}",
4
}
5
}
tailwind.config.js
1
/** @type {import('tailwindcss').Config} */
2
module.exports = {
3
content: [
4
"src/**/*.{js,ts,jsx,tsx}",
5
"app/**/*.{js,ts,jsx,tsx}",
6
"pages/**/*.{js,ts,jsx,tsx}"
7
],
8
theme: {
9
extend: {},
10
},
11
plugins: [require('@tailwindcss/typography'),],
12
}

app dir下に {layout,head,page}.tsx を追加

各種設定の変更を終えたのちに、/app/{layout,head,page}.tsx を作成してみる。なお、v12 で pages/index.tsx に相当するものは、app/page.tsx になった。

/app/page.tsx
1
export default function Page() {
2
return <div>app/page.tsx</div>
3
}

npm run dev

image
minimum /app/page.tsx

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

image
pages/hoge.tsx

TailwindCSS の globals.scss/page/layout.tsx で import すると

/app/layout.tsx
1
import "../src/styles/globals.scss"
2
3
export default function RootLayout({ children }) {
4
return (
5
<html lang="ja">
6
<body>{children}</body>
7
</html>
8
);
9
}
image
import scss from /src/styles/ at app/page.tsx

Data fetching と Static Genrateの修正

Next.js APIs such as getServerSideProps, getStaticProps, and getInitialProps are not supported in the new app directory.

Data Fetching: Fundamentals | Next.js

v13 からは generateStaticParamsasync function getData()(任意の関数名)が使われるようになった。

投稿一覧ページ

/app/page.tsx
1
import { getPostsData } from "@src/utils/markdown/getContentData"
2
3
async function getData() {
4
const { posts } = await getPostsData();
5
return posts;
6
}
7
8
export default async function Page() {
9
const posts = await getData();
10
11
return (
12
<>
13
<div className="text-red-500">app/page.tsx</div>
14
<pre>
15
{JSON.stringify(posts, null, 2)}
16
</pre>
17
</>
18
)
19
}
image
get posts at /app/page.tsx

投稿詳細ページ

v12 までの投稿詳細ページでは dynamic routes の Catch all routes を利用し、/pages/entry/[...slug].tsx となっていた。v13 からの app/ 下では /pages/entry/[...slug]/page.tsx となる。

/app/posts/[...slug]/page.jsx
1
export default function Page({ params, searchParams }) {
2
return (
3
<>
4
<p>{JSON.stringify(params.slug, null, 2)}</p>
5
<p>{JSON.stringify(searchParams, null, 2)}</p>
6
</>
7
);
8
}

v13 app/ 環境下での TypeScript が公式で実装途中なために自分で型定義しないといけないと言うこと以外は、基本的に v12 以前と同じ感じ。

image
minimum catch-all dynamic routes
/app/posts/[...slug]/page.jsx
1
import { getPostsData } from "@src/utils/markdown/getContentData";
2
3
export async function generateStaticParams() {
4
const { posts } = await getPostsData();
5
const params = posts.map(({ fileName }) => {
6
return {
7
slug: fileName.split("/")
8
}
9
})
10
return params
11
}
12
13
async function getData(params: any) {
14
const fileName = params.slug.join("/");
15
const { posts } = await getPostsData();
16
const post = posts.find((post) => post.fileName === fileName);
17
return post
18
}
19
20
export default async function Page({ params, searchParams }) {
21
const post = await getData(params)
22
return <p>{JSON.stringify(post, null, 2)}</p>
23
}
image

3rd party パッケージを適切にラッピングする

今回の Next.js v13 から useEffectuseState などクライアントで動く 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 を含めた形でラッパーを作る必要がある。

next-mdx-remote
1
type Props = {
2
compliedSource: string;
3
components: Record<string, (props: any) => JSX.Element>
4
}

なので

next-mdx-remote.tsx
1
"use client";
2
3
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote'
4
import { MDXComponents } from './mdx-components';
5
6
type Props = Pick<MDXRemoteSerializeResult, "compiledSource">;
7
8
export const NextMDXRemote: React.FC<Props> = ({ compiledSource }) => (
9
<MDXRemote components={MDXComponents} compiledSource={compiledSource} />
10
)

これで無事動くようになった。

その他

next/head<Head /> が無くなり、代わりに head.tsx で指定するようになったのだが挙動が怪しい。production 環境用にはちょっとまだ時期尚早かな感あった。

参照