コンテンツにスキップ

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でCookieの書き換えが使えない

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

Error [TurbopackInternalError]: Permission denied (os error 13)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
▲ Next.js 16.1.6 (Turbopack)
- Local:         http://localhost:3000
- Network:       http://172.17.0.2:3000
- Environments: .env.local

✓ Starting...

-----
FATAL: An unexpected Turbopack error occurred. A panic log has been written to /tmp/next-panic-bb86a2be37d62e5dc3b48cdd3aa62819.log.

To help make Turbopack better, report this error by clicking here.
-----

Error [TurbopackInternalError]: Permission denied (os error 13)
    at <unknown> (TurbopackInternalError: Permission denied (os error 13)) {
  location: undefined
}

他のユーザーが作った.nextを読み書きできないために起きている。.nextを削除してもう一度試す。

Standaloneモード

next.config.js: output | Next.js

next.config.tsに以下のオプションを設定することでStandaloneモードが有効になる。

next.config.ts
1
2
3
module.exports = {
  output: "standalone",
};

.next/standalone にフォルダが作成され、中にnode_modulesやアプリケーション実行のためのserver.jsが生成される。

以下については仕様上コピーされないので、自分でコピーする。

  • public
  • .next/static
1
2
3
4
5
6
7
8
.next/standalone
├── .next
│   └── static
├── node_modules
├── public
├── .env
├── package.json
└── server.js

これにより、nodeコマンドのみで実行できるようになる。Dockerイメージ作成時にStandaloneモードを使えばイメージの軽量化が期待できる。

1
$ node .next/standalone/server.js

CSS Modulesを型安全に使う

1
$ npm i -D typescript-plugin-css-modules
tsconfig.json
1
2
3
4
5
{
  "compilerOptions": {
    "plugins": [{ "name": "typescript-plugin-css-modules" }]
  }
}

VSCodeを使っている場合、デフォルトではVSCodeにバンドルされているTypeScriptが使われるのでインストールしたプラグインを読み込まない。

ワークスペースのTypeScriptを使うようにすることでプラグインが機能するようになる。

.vscode/settings.json
1
2
3
{
  "js/ts.tsdk.path": "node_modules/typescript/lib"
}
  1. Ctrl+Shift+Pでコマンドパレットを開く
  2. TypeScript: Select TypeScript Versionを実行する
  3. Use Workspace Versionを選択