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.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
- 2022
public/
やったこと
Next.js や ESLint など各種パッケージのアップデートはガイド通りなので省略する。
app
dirの有効化と各種 .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 環境用にはちょっとまだ時期尚早かな感あった。