Closed11

Next.js v12ベースからv14まで移行する

たにしたにし

このスクラップについて

タイトル通り。
page router で構築されているアプリケーションをNextjs v14まで移行させる。

想定しているステップは以下の通り

  1. Nextjsv13のままapp router対応を進める 参考:app-router-migration
  2. Nextjs v14まで上げ切る

https://zenn.dev/yumemi_inc/articles/next-13-app-overview

https://nextjs.org/docs/app/building-your-application/upgrading/codemods

https://nextjs.org/blog/next-13-4

注意しそうなライブラリ等

https://mui.com/base-ui/guides/next-js-app-router/
https://chakra-ui.com/getting-started/nextjs-guide#app-directory-setup

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時代のデータ取得アーキテクチャ

app routerのメリット

Rendering: Server Components

実際に移行して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 で参照している場合、そこが反映されない。

migration-v13.py
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を移植する

イメージ以下

before
const App = ({ Component, pageProps }: AppProps) => {
  return (
      <FooProvider theme={theme}>
          <Component {...pageProps} />
      </FooProvider>
  );
};
after
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に変更する

https://nextjs.org/docs/messages/built-in-next-font

バージョン 13.2 以降では、next/fontNext.js に組み込まれており、@next/fontパッケージが冗長になっています。この@next/fontパッケージは Next.js 14 で完全に削除されます。

npx @next/codemod built-in-next-font .

そうすると自動で差し替えてくれる

たにしたにし

上記を進めていくとcompile errしなくなったが、tailwind が効かなくなった

以下を参考に設定を変更
https://nextjs.org/docs/app/building-your-application/styling/tailwind-css

app 環境に合わせて修正

tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './src/**/*.{js,ts,jsx,tsx,mdx}', // app 対応
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
app/globals.css
@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 (
  ....
  )
}

`

たにしたにし

login画面 など、app/layoutを継承させたくない時

https://nextjs.org/docs/app/building-your-application/routing/route-groups

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同様各ページに対応するように設定する

たにしたにし

アップグレード作業はここで完了。
効果的に活用できているかを以下で確認する

たにしたにし

font変更
https://zenn.dev/hayato94087/articles/f6557abbd6d079

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>
  )
}
このスクラップは2023/12/21にクローズされました