コンテンツにスキップ

Next.js を始める

Next.js は React ベースの Web アプリケーションフレームワークである。

プロジェクトを作成する

1
$ npx create-next-app@latest

テレメトリの収集を無効化する

1
$ npx next telemetry disable

デバッグ実行

1
$ npm run dev

ルーティング

【完全版】App Router で最初に知っておくとよさそうな基礎を全部まとめてみた - らくらくエンジニア

appディレクトリ内にディレクトリを作成し、そこにpage.tsxを作成すると 1 ページになる。

ページのコンポーネントはexport defaultにする必要がある。

1
2
3
4
5
6
.
└── src
     └── app
         ├── diary
         │   └── page.tsx
         └── page.tsx
src/app/page.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import Link from "next/link";

export default function Home() {
  return (
    <>
      <h1>Home</h1>
      <Link href="/diary">Diary</Link>
    </>
  );
}
src/app/diary/page.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import Link from "next/link";

export default function Diary() {
  return (
    <>
      <h1>Diary</h1>
      <Link href="..">Home</Link>
    </>
  );
}

レイアウト

レイアウトを使うとその配下のページ共通のレイアウトを適用できる。

src/app/diary/layout.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Link from "next/link";
import React from "react";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <header>
        <h1>Diary</h1>
        <Link href="..">Home</Link>
        <nav>
          <ul>
            <li>
              <Link href="2023.html">2023</Link>
            </li>
            <li>
              <Link href="2024.html">2024</Link>
            </li>
            <li>
              <Link href="2025.html">2025</Link>
            </li>
          </ul>
        </nav>
      </header>
      <main>{children}</main>
    </>
  );
}
src/app/diary/page.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export default function Diary() {
  return (
    <>
      <section>
        <h2>記事</h2>
        <p>内容</p>
      </section>
      <section>
        <h2>記事</h2>
        <p>内容</p>
      </section>
    </>
  );
}

アプリケーション起動時の処理を書く

Guides: Instrumentation | Next.js

src/instrumentation.tsregister()を実装する。

src/instrumentation.ts
1
2
3
export function register() {
  console.log("launched");
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ npm run dev

> nextjs-example2@0.1.0 dev
> next dev --turbopack

    Next.js 15.3.1 (Turbopack)
   - Local:        http://localhost:3000
   - Network:      http://10.255.255.254:3000

  Starting...
  Compiled instrumentation Node.js in 15ms
  Compiled instrumentation Edge in 23ms
launched
  Ready in 746ms

React Server Components (RSC)

Next.js でコンポーネントを作成すると基本的に React Server Components (RSC)になる。

厳密にはsrc/appにあり、use clientを書いていないコンポーネントは RSC になるらしい。

RSC はサーバー上で作成される。

RSC を非同期関数にして、await で関数を呼べばその処理はサーバーサイドで実行される。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export async function getData() {
  console.log("getData() called");
  return { key: "value" };
}

export default async function Home() {
  const data = await getData();
  return (
    <>
      <h1>Hi</h1>
      <p>{JSON.stringify(data)}</p>
    </>
  );
}

Remix でいう loader()

上記の通り RSC を非同期関数にして await で関数を呼べばよい。

getServerSidePropsというのもあるがこれは古いやり方らしく、App Router では使えない。

Client-side Rendering (CSR)

useState()useEffect()を使いたい場合はクライアントコンポーネントとして作成する。

ファイルの行頭に"use client";を付与すればクライアントコンポーネントにできる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
"use client";

import { useState } from "react";

export function Counter() {
  const [value, setValue] = useState(0);

  function handleClick() {
    setValue((v) => v + 1);
  }

  return <button onClick={handleClick}>{value}</button>;
}

Remix でいう action()

  • RSC で form を使う。
  • RSC にクライアントコンポーネントを埋め込み、クライアントコンポーネントから API をコールする。

デプロイ

npm run build && npm run startで良いらしい。

Docker コンテナで動作させる

とりあえず以下で動く。

Dockerfile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
FROM node:22.13.1

WORKDIR /app

# パッケージファイルをコピーする
COPY package.json package-lock.json ./

# パッケージをインストールする(--omit=devを使うとdevDependenciesを除けるが、TypeScriptがインストールされないので使わない)
RUN npm ci

# ソースコードをコピーする
COPY . ./

# ビルドする
RUN npm run build

# 実行する
CMD ["npm", "run", "start"]

# ポートを公開する
EXPOSE 3000

環境変数を使う

Guides: Environment Variables | Next.js

.env.env.localを作成すればprocess.env.XXXで取得できる。

API を作成する

App Router ではあまり使わないかもしれない。

Routing: API Routes | Next.js

src/app/api/diary/route.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { NextResponse } from "next/server";

export interface DiaryGetResponse {
  message: string;
}

export async function GET(): Promise<NextResponse<DiaryGetResponse>> {
  return NextResponse.json({
    message: "Hi!",
  });
}

パスパラメータ・クエリパラメータを取得する

パスパラメータは[id]のようなディレクトリを作成する。

RSC の props からパスパラメータとクエリパラメータを取得できる。

1
2
3
4
5
6
7
.
└── src
     └── app
          └── diary
               ├── [id]
               │   └── page.tsx
               └── page.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { getTranslations } from "next-intl/server";

export default async function IdDiary(props: {
  params: { id: string };
  searchParams?: { q: string };
}) {
  const t = await getTranslations();

  return (
    <>
      <h2>{t("diary")}</h2>
      <p>{props.params.id}</p>
      <p>{JSON.stringify(props.params)}</p>
      <p>{JSON.stringify(props.searchParams)}</p>
    </>
  );
}

例外

RSC での例外はtry-catchで囲む。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { getTranslations } from "next-intl/server";
import { fetchArticle } from "../actions";

export default async function IdDiary(props: {
  params: { id: string };
  searchParams?: { q: string };
}) {
  try {
    const t = await getTranslations();
    const article = await fetchArticle(props.params.id);

    return (
      <>
        <h2>{t("diary")}</h2>
        <h3>{article.title}</h3>
        <div dangerouslySetInnerHTML={{ __html: article.content }} />
      </>
    );
  } catch (error) {
    return <>Error</>;
  }
}

パスを取得する

以下で初回レンダリング時には取得できる。

src/middleware.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  const url = request.nextUrl.clone();
  const response = NextResponse.next();
  const path = new URL(url).pathname;
  response.headers.set("x-url", url.toString());
  response.headers.set("x-path", path);
  return response;
}

export const config = {
  matcher: "/:path*",
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import type { Metadata } from "next";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";
import Link from "next/link";
import "./globals.css";

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const t = await getTranslations();
  const locale = await getLocale();
  const h = await headers();
  const path = h.get("x-path");
  console.log("path", path);

  return (
    <html lang={locale}>
      <body></body>
    </html>
  );
}

ページを遷移するたびの取得はクライアントコンポーネントでusePathname();を使う方法しかないらしい。

多言語に対応する

Next.js で多言語対応

ログイン・ログアウト処理を作成する

ダークモードに対応する

Web フォントを利用する

publicディレクトリに Web フォントを置く。

src/app/globals.css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@font-face {
  font-family: NotoSansJP;
  src: url("/NotoSansJP-VariableFont_wght.ttf") format("truetype");
}

@font-face {
  font-family: NotoSerifJP;
  src: url("/NotoSerifJP-VariableFont_wght.ttf") format("truetype");
}

body {
  font-family: NotoSansJP, sans-serif;
}

package.jsonのバージョンを表示する

next.config.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import type { NextConfig } from "next";
import { version } from "./package.json";

const nextConfig: NextConfig = {
  /* config options here */
  env: {
    VERSION: version,
  },
};

export default nextConfig;

process.env.VERSIONで取得する。

cookies()で取得できる。

1
2
3
4
import { cookies } from "next/headers";

const c = await cookies();
const theme = c.get("theme")?.value ?? "light";

Warning: 'error' is defined but never used. @typescript-eslint/no-unused-varsを無視する

eslint.config.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname,
});

const eslintConfig = [
  ...compat.extends("next/core-web-vitals", "next/typescript"),
  {
    ignores: [
      "node_modules/**",
      ".next/**",
      "out/**",
      "build/**",
      "next-env.d.ts",
    ],
    rules: {
      "@typescript-eslint/no-unused-vars": "off",
    },
  },
];

export default eslintConfig;

Error: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-anyを無視する

eslint.config.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname,
});

const eslintConfig = [
  ...compat.extends("next/core-web-vitals", "next/typescript"),
  {
    ignores: [
      "node_modules/**",
      ".next/**",
      "out/**",
      "build/**",
      "next-env.d.ts",
    ],
    rules: {
      "@typescript-eslint/no-unused-vars": "off",
      "@typescript-eslint/no-explicit-any": "off",
    },
  },
];

export default eslintConfig;

"use server";import "server-only";の違い

"use server";はサーバーアクションの入口であることの宣言。コードはサーバーサイドで実行される。

import "server-only";はクライアントから呼び出せなくなる。コードはサーバーサイドで実行される。

Server Actions でのエラーハンドリング

throw でエラーを投げることができるが、クライアントができるのは catch だけで内容はわからない。

Result 型と呼ばれるインターフェースを定義するのが一般的らしい。

API を作成する

1
2
3
4
5
└── src
     └── app
         └── api
              └── hi
                    └──route.ts
src/app/api/hi/route.ts
1
2
3
export async function GET() {
  return Response.json({ message: "Hi" });
}

トラブルシューティング

ビルド時にbecause it took more than 60 seconds.で失敗する

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11.21    Collecting page data ...
12.37    Generating static pages (0/6) ...
13.24    Generating static pages (1/6)
13.24    Generating static pages (2/6)
13.24    Generating static pages (4/6)
72.83 Failed to build /hoge/page: /hoge (attempt 1 of 3) because it took more than 60 seconds. Retrying again shortly.
133.4 Failed to build /hoge/page: /hoge (attempt 2 of 3) because it took more than 60 seconds. Retrying again shortly.
194.6 Failed to build /hoge/page: /hoge after 3 attempts.
194.6 Export encountered an error on /hoge/page: /hoge, exiting the build.
194.6   Next.js build worker exited with code: 1 and signal: null

Static page generation timed out after multiple attempts | Next.js

ビルド時に 60 秒以上かかるとタイムアウトで失敗扱いになる。タイムアウト時間を延ばす。以下は 600 秒にする設定例である。

next.config.ts
1
2
3
4
5
6
7
8
9
import type { NextConfig } from "next";
import { version } from "./package.json";

const nextConfig: NextConfig = {
  /* config options here */
  staticPageGenerationTimeout: 600,
};

export default nextConfig;

Cannot use() an already resolved Client Reference.というエラーが出る

クライアントコンポーネントをサーバーコンポーネントとしてレンダリングしている可能性がある。"use client";を指定する。

redirect()がうまく動かない

redirect()が例外を投げて実現する仕組みのため、try-catchで囲んでいるとうまく動かない。redirect()try-catchで囲っている場合、外す。

RSC では無理。Server Actions か Route Handlers(API)を使うこと。