JavaScript
Next.js
TypeScript
public
Next.jsを始める
Next.jsはReactベースのWebアプリケーションフレームワークである。
プロジェクトを作成する
$ npx create-next-app@latest
テレメトリの収集を無効化する
$ npx next telemetry disable
デバッグ実行
ルーティング
【完全版】App Routerで最初に知っておくとよさそうな基礎を全部まとめてみた - らくらくエンジニア
appディレクトリ内にディレクトリを作成し、そこにpage.tsxを作成すると1ページになる。
ページのコンポーネントはexport defaultにする必要がある。
.
└── src
└── app
├── diary
│ └── page.tsx
└── page.tsx
src/app/page.tsx import Link from "next/link" ;
export default function Home () {
return (
<>
< h1 > Home </ h1 >
< Link href = "/diary" > Diary </ Link >
</>
);
}
src/app/diary/page.tsx 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.tsにregister()を実装する。
src/instrumentation.ts 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 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からパスパラメータとクエリパラメータを取得できる。
.
└── 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 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で取得する。
Cookieを使う
cookies()で取得できる。
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を作成する
└── src
└── app
└── api
└── hi
└──route.ts
src/app/api/hi/route.ts export async function GET () {
return Response . json ({ message : "Hi" });
}
トラブルシューティング
ビルド時にbecause it took more than 60 seconds.で失敗する
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 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 module .exports = {
output : "standalone" ,
};
.next/standalone にフォルダが作成され、中にnode_modulesやアプリケーション実行のためのserver.jsが生成される。
以下については仕様上コピーされないので、自分でコピーする。
.next/standalone
├── .next
│ └── static
├── node_modules
├── public
├── .env
├── package.json
└── server.js
これにより、nodeコマンドのみで実行できるようになる。Dockerイメージ作成時にStandaloneモードを使えばイメージの軽量化が期待できる。
$ node .next/standalone/server.js
CSS Modulesを型安全に使う
$ npm i -D typescript-plugin-css-modules
tsconfig.json {
"compilerOptions" : {
"plugins" : [{ "name" : "typescript-plugin-css-modules" }]
}
}
VSCodeを使っている場合、デフォルトではVSCodeにバンドルされているTypeScriptが使われるのでインストールしたプラグインを読み込まない。
ワークスペースのTypeScriptを使うようにすることでプラグインが機能するようになる。
.vscode/settings.json {
"js/ts.tsdk.path" : "node_modules/typescript/lib"
}
Ctrl+Shift+Pでコマンドパレットを開く
TypeScript: Select TypeScript Versionを実行する
Use Workspace Versionを選択