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)を使うこと。