Next.js v12ベースからv14まで移行する
このスクラップについて
タイトル通り。
page router で構築されているアプリケーションをNextjs v14まで移行させる。
想定しているステップは以下の通り
- Nextjsv13のままapp router対応を進める 参考:app-router-migration
- Nextjs v14まで上げ切る
注意しそうなライブラリ等
MUI, Chakra UIとかは一時期対応していなかったが今はしている
(これはEmotion に依存しているから。Emotionはクライアント側で動作する前提 issueとか)
app routerとは
- app というディレクト名から来ている(従来は Page Router
- で、こいつはServer Component をデフォルトとして動いている
- SCでは、Reactアプリケーションの中に、サーバーで実行される部分とクライアント側で実行される部分がある
- 競争力上げるためにサービスのUX上げていく(という流れ
- UX上げるにはJSが必要だけど、全部Reactでやるとオーバヘッドあるよね。(バンドルサイズ膨れるし、first view遅くなるし)
- じゃあそれと関係ないものはserver で生成してもいいよね
- という課題からきていると認識している
- なのでPHPとかDjango で生成するSCとは見た目は同じようだけど意味が異なる(reactでやっているので
- なので、Nextjs特有というわけではなく サーバー とクライアント で実行されるようになったよ。という暗黙の意味もapp routerには含んでいる
初見だと何ってるかわかんねーかもなので以下を見るといいかも
app routerのメリット
実際に移行してfirst viewが早くなったことなどもすでに感じられる
app router migrate
yarn upgrade next@13.4.0
step1. page ディレクトリを appに変更する && next/router のreplace
page数も多いのでscriptを作成した
実行すると以下のような変更がされる
・pages/** → app/**
・index.tsxに関しては、'useClient付与'
・対象componentの'next/router' を 'next/navigation' に変換
・import Head from 'next/head' を import { Metadata } from 'next' に置換までやってくれる
注意点が、index.tsx からexport して 他component で参照している場合、そこが反映されない。
import os
import shutil
def main():
rename_page_to_app()
rename_index_files()
update_tsx_files_in_directory('../src/features')
update_tsx_files_in_directory('../src/components')
def rename_page_to_app():
source_dir = '../src/pages'
dest_dir = '../src/app'
if os.path.exists(source_dir):
shutil.move(source_dir, dest_dir)
print(f"Directory {source_dir} renamed to {dest_dir}")
else:
print(f"Directory {source_dir} does not exist!")
def rename_index_files():
start_dir = '../src/app'
target_name = 'index.tsx'
for foldername, _, filenames in os.walk(start_dir):
for filename in filenames:
if filename == target_name:
source_path = os.path.join(foldername, filename)
with open(source_path, 'r') as file:
content = file.read()
content = "'use client'\n" + content
content = content.replace('next/router', 'next/navigation')
content = content.replace("import Head from 'next/head'", "import { Metadata } from 'next'")
with open(source_path, 'w') as file:
file.write(content)
dest_path = os.path.join(foldername, 'page.tsx')
shutil.move(source_path, dest_path)
print(f'File {filename} moved to {dest_path}')
print("All index.tsx files have been renamed to page.tsx")
def update_tsx_files_in_directory(target_dir):
for foldername, _, filenames in os.walk(target_dir):
for filename in filenames:
if filename.endswith('.tsx') or filename.endswith('.ts'):
source_path = os.path.join(foldername, filename)
with open(source_path, 'r') as file:
content = file.read()
# use client は手動で追加
content = content.replace('next/router', 'next/navigation')
content = content.replace("import Head from 'next/head'", "import { Metadata } from 'next'")
with open(source_path, 'w') as file:
file.write(content)
print(f'File {filename} updated')
if __name__ == "__main__":
main()
この段階ではapp/layout.tsxがないと動作しない。
app/layout.tsx => v12で言う _app.tsx + _document.tsx という理解でok
step2. _app.tsxと_document.tsx をapp/layout.tsxに移行する
おそらく、Nextjsのversionを上げた段階ですでに作成されていると思う(差分入ってた
そこに対して既存のapp.tsxとdocument.tsxを移植する
イメージ以下
const App = ({ Component, pageProps }: AppProps) => {
return (
<FooProvider theme={theme}>
<Component {...pageProps} />
</FooProvider>
);
};
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<RootProvider>
{children}
</RootProvider>
</body>
</html>
)
}
既存のwarpしているProvider系はrootProviderとして切り出し、そこで格納するようにした
routerの移行
// before
const router = useRouter();
const pathname = router.pathname;
// after
const pathname = usePathname();
// before
const router = useRouter()
const id = router.query.id
// after
const searchParams = useSearchParams();
const pageNum = searchParams.get("page");
step4 getServerSidePropsとgetStaticPropsの移行
Server component上ではより簡単に取得できるようになった
実際のコードは以下を見た方が早い。コード上はめちゃシンプルになった。
Upgrading: App Router Migration
基本はServerComponent (SC)でfetchを使っていくわけだが、
ユーザーの動作に対してサーバーにfetchしていくもの(ex検索フォーム)に関しては Client Component(CC) でfetchしていく
Rendering: Composition Patterns
CC から SC にstateを渡すことは、cookieやクエリパラメータを使って渡せばできる。
が、SCの利点を考慮すると個人的には CCでいい気がする(クエリで持たせることができるならSC)
warn - Your project has `@next/font` installed as a dependency, please use the built-in `next/font` instead.
The `@next/font` package will be removed in Next.js 14.
You can migrate by running `npx @next/codemod@latest built-in-next-font .`.
Read more: https://nextjs.org/docs/messages/built-in-next-font
@next/fontからnext/fontに変更する
バージョン 13.2 以降では、next/fontNext.js に組み込まれており、@next/fontパッケージが冗長になっています。この@next/fontパッケージは Next.js 14 で完全に削除されます。
npx @next/codemod built-in-next-font .
そうすると自動で差し替えてくれる
上記を進めていくとcompile errしなくなったが、tailwind が効かなくなった
以下を参考に設定を変更
app 環境に合わせて修正
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.{js,ts,jsx,tsx,mdx}', // app 対応
],
theme: {
extend: {},
},
plugins: [],
}
@tailwind base;
@tailwind components;
@tailwind utilities;
import
// 略..
// 追加
import './globals.css'
export const metadata: Metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
....
)
}
`
OCG関連
以下の記事がとてもわかりやすいので読むといいかも
要は、
- favicon, OG画像は配置だけで良い
- metaタグはlayoutで export const metadata でok
めちゃ楽になった
login画面 など、app/layoutを継承させたくない時
app
├─ layout.tsx # ①共通レイアウト
├─ (app) #アプリケーションページ
├─ layout.tsx # A専用レイアウト
├─ page.tsx
├─ (auth) # 認証
├─ layout.tsx # ②認証用レイアウト
├─ page.tsx
ログイン系画面、管理画面、一般ユーザー画面で分けたい時に使用する
// 略
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{children}
</body>
</html>
)
}
・シンプルにhtmlタグとbodyタグのみを構成
・無駄なレンダリングを抑える
export default function AppLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<LoginLayout>
{children}
</LoginLayout>
)
}
・従来のlayout同様各ページに対応するように設定する
yarn upgrade next@14.0.1
- Nodejsのversion が 18.17.0 以上でないとinstallできない
アップグレード作業はここで完了。
効果的に活用できているかを以下で確認する
404 ページのカスタム
app/not-found.tsx にrenameしてやれば良い
font変更
import { Noto_Sans_JP } from "next/font/google";
const notoSansJP = Noto_Sans_JP({
weight: "400",
subsets: ["latin"],
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={notoSansJP.className}>
<RootProvider>
{children}
</RootProvider>
</body>
</html>
)
}