next.jsに入門する

公式を読みながら Next.js に入門する
リポジトリ

セットアップ
App Router vs Pages Router
最新のは App Router で公式もこちらの使用を進めている。
App Router はReactの最新機能を使うことができるらしい。Pages Router は昔からあるプロジェクト用に残されていて、基本的にこれから新しくプロジェクトを始める場合は App Router で良さそう。
ディレクトリ構成
ルート直下の構成はこんな感じ。
.
├── README.md
├── app
├── next.config.mjs
├── node_modules
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock
app配下の構成はこんな感じ。基本はこのディレクトリ直下を触ることになる。
.
├── favicon.ico
├── globals.css
├── layout.tsx
└── page.tsx
layout.tsx
共通のUIコンポーネントを定義する場所。
page.tsx
App Routerでは、app直下のpage.tsxがルーティングの対象になるみたい。
例えば/about
を作りたい場合、app/about/page.tsxを作成することになる。
また/blog/1
や/blog/2
のように動的にルーティングする場合、app/blog/[blogId]/page.tsxを作成する。
注意点は、app直下にディレクトリだけを作成した段階ではルーティングされないということ。
page.tsxが作成されて初めてルーティングされる。

サーバーコンポーネントとクライアントコンポーネント
サーバーコンポーネント
- サーバー側でレンダリングされる
- デフォルトがサーバーコンポーネント
- メリット
- パフォーマンス向上
- JSのバンドルサイズが小さくなる
- トークンやAPIキーなど機密情報をクライアントに公開しない
- 検索エンジンの最適化
- デメリット
- useStateやブラウザAPI、ユーザーイベントが使えない
- 初期ページ以外はCCの方が早い可能性がある
クライアントコンポーネント
- 従来の React コンポーネントと同様
- クライアント側でレンダリングされる
- ファイル先頭に 'use client' をつけることでクライアントコンポーネントになる
詳細はいかの記事がわかりやすい。
使い分け
基本的にNextが推奨するサーバーコンポーネントを使用するようにし、必要に応じてCCを使用するのが良いか。
TIps的に、クライアントコンポーネントはできるだけ小さくするのが良さそう。
Appendix
サーバーコンポーネントで console.log してもコンソール上に表示されないことがわかる。どこに出力されるのかというと、ターミナル上で出力される。これをみるとサーバー側でレンダリングされていることがわかる。

App Router ルーティング概略
基本のルーティング
appディレクトリ以下のpage.tsxがルートページとなる。
appディレクトリ以下にディレクトリを追加し、page.tsxを作成すると、ディレクトリ名がそのままパスになる。
注意点
- ページコンポーネント名は必ずpage.tsxにする必要がある
- page.tsx内のルートコンポーネントは default export が必要
動的なルーティング
ディレクトリ名を[id]
(パラメータ名は任意)とすればOK。
また、idにあたるパラメータはpage.tsxのpropsで取得できる。
const TaskEditIdPage = ({ params }: { params: { id: string } }) => {
return <div>{params.id}</div>;
};
export default TaskEditIdPage;
ちなみに階層はこんな感じ。
.
└── app
├── page.tsx
└── task
├── edit
│ ├── [id]
│ │ └── page.tsx
│ └── page.tsx
└── page.tsx
パスに反映したくない
appディレクトリ以下にディレクトリを作成するとパスに反映されてしまう。
その場合、ディレクトリを(main}
とすればパスに main を反映しないようにできる。

共通レイアウト
layout.tsx で共通レイアウトを定義できる。
layout.tsx はネストして配置可能。
例えば、appディレクトリ以下にlayout.tsxを配置した場合、appディレクトリ以下の全てのpage.tsxに共通のレイアウトが適用される。
また、app/taskディレクトリに layout.tsx を配置すると、app/layout.tsxのレイアウトも反映しつつ、app/task/layout.tsxのサブレイアウトも反映することができる。
$ tree
.
└── app
├── favicon.ico
├── globals.css
├── layout.tsx
├── page.tsx
└── task
├── edit
│ ├── [id]
│ │ └── page.tsx
│ └── page.tsx
├── layout.tsx
└── page.tsx
こちらの例。
ルート👇
/task👇

エラーコンポーネント
Not Found
Next.js はデフォルトでNotFoundページが用意されている。
独自でカスタマイズしたい場合は not-found.tsx を作成すれば良い。
Error page
エラーがスローされた時に表示するコンポーネントは error.tsx で定義する。
error.tsx はネストが可能であり、エラーがスローされた際、そのページから最も近い error.tsx が表示される。

ルートハンドラー
APIをNext.js上で直接開発できる機能。
app/api/を作成して定義するやり方が一般的。ファイル名は route.ts とする必要がある。
例えば以下のディレクトリ構成にして、
$ tree
.
└── app
├── (admin)
│ ├── dashboard
│ │ ├── error.tsx
│ │ └── page.tsx
│ └── layout.tsx
├── (main)
│ ├── page.tsx
│ └── task
│ ├── edit
│ │ ├── [id]
│ │ │ └── page.tsx
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
├── api
│ └── tasks
│ └── route.ts
├── cc
│ └── page.tsx
├── error.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
├── not-found.tsx
└── sc
└── page.tsx
app/api/tasks/route.ts で、次のように定義した時、
import { NextResponse } from "next/server";
export interface Task {
id: number;
name: string;
}
const tasks: Task[] = [
{ id: 1, name: "プログラミング" },
{ id: 2, name: "ランニング" },
];
export const GET = async () => {
return NextResponse.json(
{ tasks },
{
status: 200,
}
);
};
export const dynamic = "force-dynamic";
http://localhost:3000/api/tasks
にアクセスすると結果が返ってくる。
{
"tasks":[
{
"id":1,
"name":"プログラミング"
},
{
"id":2,
"name":"ランニング"
}
]
}
ルートハンドラはデフォルトでキャッシュされない。キャッシュ有効かする場合、export const dynamic = 'force-static'
とする。

ローディングコンポーネント
ローディングコンポーネントのファイル名は loading.tsx
とする。

Server Actions
一般的にボタンクリックでユーザーイベントが発火し、fetch関数などでAPIエンドポイントにアクセスしCRUD処理を行う流れになる。
ServerActionsでは、APIエンドポイントを介さずユーザーイベントの発火で直接CRUD処理が実現できる。
この機能、すごすぎぃ。
ServerActionsを定義するファイルとフォームを定義するコンポーネントファイルは分離するのが良い。
フォームコンポーネント👇
import { createTask } from "@/actions/sampleActions";
const ServerActionsPage = () => {
return (
<div>
<form action={createTask}> // HERE!!
<input type="text" id="name" name="name" className="bg-gray-200" />
<button type="submit" className="bg-gray-400 ml-2 px-2">
送信
</button>
</form>
</div>
);
};
export default ServerActionsPage;
ServerActions定義ファイル👇
"use server";
export const createTask = async (formData: FormData) => {
// DBにタスクを作成
console.log("タスクを作成しました");
console.log(formData.get("name"));
};
フォームに適当な値を入れて送信してみるとブラウザのコンソールには出力されず、ターミナル上で出力される。これによりサーバー上で実行されていることがわかる。

Middleware
Middlewareを導入することで、ページアクセス前に認証、ロギング、リダイレクトなどを実現できる。
定義するには、src/直下にmiddleware.ts
を作成する。
import { NextRequest, NextResponse } from "next/server";
export const middleware = (request: NextRequest) => {
console.log("ミドルウェア");
return NextResponse.next();
};
export const config = {
matcher: ["/", "/task"],
};
トップもしくは/task
にアクセスすると、ターミナル上でミドルウェア
と表示されていることがわかる。configはミドルウェアを通過する対象パスを定義したオブジェクト。