Next.js は現在最も人気のある React ベースのフルスタックの JavaScript フレームワークです。バージョンがアップする毎に新しい機能が次々に追加され Next.js13 から Server Components など React の最新機能を利用した App Router が登場しました。App Router はファイル名でルーティングを設定していた既存の Page Router とは全く異なる機能で設定方法も一から学び直す必要があります。新たにプロジェクトを作成するのであれば App Router を利用することが推奨されています。同時に両方の機能を利用することも可能です。
次々に新しい機能が追加される反面、ネット上に公開されている記事もすぐに OutDated なものになっています。この文書もすぐに Outdated なものになってしまうと思いますが現在(2023 年 5 月)の最新バージョン 13.4 のドキュメントを参考に実際に Next.js13 を利用しながら基本的な機能について説明を行っています。
目次
プロジェクトの作成
実際に公式ドキュメントを参考に手を動かしながら説明を進めていくため最初に Next.js のプロジェクトの作成を行います。プロジェクト名は任意の名前をつけることができるのでここでは next-js-13 としています。npx create-next-app@latest コマンドを実行すると TypeScript, ESLint をプロジェクトで利用するかどうか聞かれますがすべてデフォルトの値を選択しています。 App Router を利用するかどうかも選択することができますが recommeded と表示されているため新しくプロジェクトを作成する場合に App Router を利用することが推奨されています。
% npx create-next-app@latest
Need to install the following packages:
create-next-app@13.4.1
Ok to proceed? (y) y
✔ What is your project named? … next-js-13
✔ Would you like to use TypeScript with this project? … No / Yes
✔ Would you like to use ESLint with this project? … No / Yes
✔ Would you like to use Tailwind CSS with this project? … No / Yes
✔ Would you like to use `src/` directory with this project? … No / Yes
✔ Use App Router (recommended)? … No / Yes
✔ Would you like to customize the default import alias? … No / Yes
Creating a new Next.js app in /Users/mac/Desktop/next-js-13.
Using npm.
Initializing project with template: app-tw
Installing dependencies:
- react
- react-dom
- next
- typescript
- @types/react
- @types/node
- @types/react-dom
- tailwindcss
- postcss
- autoprefixer
- eslint
- eslint-config-next
added 352 packages, and audited 353 packages in 32s
136 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Initialized a git repository.
プロジェクトの作成が完了後、プロジェクトフォルダ next-js-13 に移動して package.json ファイルでインストールしたパッケージのバージョンを確認しておきます。next パッケージのバージョンが 13.4 以上であることを確認しておきます。
{
"name": "next-js-13",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@types/node": "20.1.1",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4",
"autoprefixer": "10.4.14",
"eslint": "8.40.0",
"eslint-config-next": "13.4.1",
"next": "13.4.1",
"postcss": "8.4.23",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "3.3.2",
"typescript": "5.0.4"
}
}
package.json ファイルを確認後、npm run dev コマンドを実行して開発サーバを起動してブラウザからアクセスを行い、初期ページが表示されることを確認します。初期画面に表示されている内容は app ディレクトリの直下に保存されている page.tsx ファイルに記述されています。
トップページの表示
Routing の設定
これまで利用してきたファイルシステムベースのルーティングは Pages Router と呼ばれ Next.js からは App Router という新機能が登場しました(Next.js 13.4 から Stable)。Pages Router と App Router では設定方法が異なります。どちらの機能も利用できるため Next.js のドキュメント上では App Router、Pages Router を切り替えることでそれぞれの Router の機能と設定方法を確認することができます。App Router のドキュメントを確認する場合は"Using App Router"を選択して読み進めてください。
Next.js のドキュメント
”Routing の設定”では新機能の App Router について説明を行なっていきますが Pages Router との設定方法の違いを確認するため最初に Pages Router でのルーティング設定方法も確認しておきます。
Pages Router
インストール時に App Router を利用することを選択しましたが Pages Router も引き続き利用することができます。Pages Router では pages ディレクトリの下にルーティングのファイルを作成していくため新たにプロジェクトディレクトリ直下に pages ディレクトリを作成することから始めます。pages ディレクトリの作成が完了したら about.tsx ファイルを作成します。
const About = () => {
return <div>About</div>;
};
export default About;
ファイルを作成するだけで Next.js が自動でルーティングが設定してくれるのでファイル名がそのまま URL となります。ブラウザから http://localhost:3000/about にアクセスすると about.tsx ファイルで記述した内容がそのままページに表示されます。このように Pages Router ではファイル名と URL が連動します。/about/index.tsx ファイルと設定することも可能です。
Pages Router での about ページの表示
App Router
App Router では app ディレクトリの下にファイルを作成していきます。プロジェクト作成時に App Router の利用を選択しているためデフォルトで app ディレクトリは作成されています。
Pages の設定
/about のルーティングを設定するためにはファイルではなく about ディレクトリを app ディレクトリの下に作成する必要があります。その後 about ディレクトリの下に page.tsx ファイルを作成します。ファイル名は TypeScript であれば page.tsx または page.ts、JavaScript では page.jsx または page.js とする必要があります。
const Page = () => {
return <div>About</div>;
};
export default Page;
ファイルを保存すると App Router と Page Router で別々に設定した about がコンフリクト(衝突)しているということで npm run dev コマンドを実行したターミナルにはエラーメッセージが表示されます。
- error Conflicting app and page file was found, please remove the conflicting files to continue:
- error "pages/about.tsx" - "app/about/page.tsx"
コンフリクトの問題を解消するために app ディレクトリの設定か page ディレクトリの設定のどちらかを変更する必要がありますがここでは pages ディレクトリを削除します。pages ディレクトリの about.tsx ファイルのファイル名を変更することでもエラーは解消されます。エラーの解消後は app ディレクトリで設定した about/page.tsx ファイルの内容がブラウザに表示されます。
App Router での about ページの表示
Layout の設定
ブラウザ上に表示された about ページの画面にはグラデーションが入っていますが page.tsx ファイルには CSS によるスタイルを設定していません。ではなぜ page.tsx ファイルに CSS が設定されていないにも関わらず CSS によるスタイルが設定されているのでしょう。その理由は app ディレクトリ下の layout.tsx ファイルの設定が反映されているためです。layout.tsx は名前の通りレイアウトに関するファイルで App Router では app ディレクトリ直下に保存された layoutx.tsx ファイルがすべての page.tsx に適用されます。layout.tsx の中身を確認します。html, body が利用されており、children の部分に page.tsx のコンテンツが挿入されます。
import './globals.css';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode,
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
layout.txs ファイルでは globals.css が import されていることがわかります。globals.css ファイルの中で body タグに対して CSS が設定されているので CSS を削除するとグラデーションは消えます。
app ディレクトリ直下の layout.txs ファイルは必須ファイルのため layout.tsx ファイルの名前を変更したり削除すると page.tsx ファイルに layout が存在しないため Next.js が自動で layout.tsx ファイルを作成します。
Nested Layout の設定
Layout ファイルは app ディレクトリ直下だけではなく Page ディレクトリの配下にある page.tsx ファイに共通の Layout を適用したい場合に Page ディレクトリの直下に layout.tsx を作成することができます。
about ディレクトリの下に layout.tsx ファイルを作成します。Tailwind CSS の Utility Class を利用して中央に表示するように設定しています。
export default function AboutLayout({
children,
}: {
children: React.ReactNode,
}) {
return (
<div className="flex justify-center items-center h-screen">{children}</div>
);
}
Nested Layout の適用
about ディレクトリの中にさらに別の Page ディレクトリを作成することができます。例えば about ディレクトリの下に info ディレクトリを作成して page.tsx ファイルを作成すると about ディレクトリに作成した layout.tsx ファイルが適用されます。about ディレクトリだけではなく app ディレクトリ直下にある layout.tsx ファイルも適用されます。
Route Group の設定
App Router では app ディレクトリにディレクトリを作成することでルーティングを作成していきますがルーティングのパスに影響を与えないディレクトリを作成することができます。その方法の一つに Route Group があります。Route Group を設定するディレクトリは()で囲む必要があります。()で囲んだディレクトリ下にはパスに影響を与えることはありませんが layout.tsx ファイルを作成することができるので Route Group 以下のすべてのページに作成した layout.tsx ファイルを適用することができます。言葉よりも実際に設定した方が理解が進むので任意の名前の marketing ディレクトリを作成します。
(marketing)ディレクトリの下には layout.tsx ファイルを作成して以下のコードを記述します。
export default function MarketingLayout({
children,
}: {
children: React.ReactNode,
}) {
return <div className="m-4 font-bold">{children}</div>;
}
(marketing)ディレクトリの直下に account ディレクトリを作成して page.tsx ファイルを作成します。
const Page = () => {
return <div>Account</div>;
};
export default Page;
ブラウザからアクセスする場合は()で囲んだディレクトリはルーティングのパスに影響を与えないので marketing を省いて/account でアクセスを行うことができます。
Account ページの表示
(marketing)ディレクトリの下には account 以外にも Page ディレクトリを作成して page.tsx ファイルを作成すると(marketing)ディレクトリの下の layout.tsx ファイルが適用されます。
Dynamic Routes の設定
ここまでの設定では about や account などルーティングが静的で URL が変わらないページの設定を行いました。しかし実際のアプリケーションでは静的な URL ばかりではなく例えばブログの記事を表示したい場合には/blog/1, /blog/2, /blog/what-is-next.js...などのように動的に変わるルーティングに対応させる必要があります。
Dynamic Routes を設定するために app ディレクトリ直下に blog ディレクトリを作成します。/blog/1, /blog/2 でアクセスするために blog ディレクトリの下にさらにディレクトリを作成しますが Dynamic Routes の場合は[]でディレクトリ名を囲みます。ここでは[id]ディレクトリを作成します。id は任意なので slug や blogId と設定することもできます。名前は任意ですが後ほどこの名前はコードの中で利用するので役割に応じた適切な名前をつけてください。
[id]ディレクトリの下に page.tsx ファイルを作成して以下のコードを記述します。
const Page = () => {
return <div className="m-4 font-bold">Blog ID:</div>;
};
export default Page;
/blog/1, /blog/2, ... /blog/100 でアクセスすることが可能になり、/blog/以下にどのような文字列を設定しても page.tsx ファイルに記述した内容が表示されます。
Dynamic Routes の設定によるページの表示
Dynamic Routes のページでは/blog/以下に指定した値は Props を利用して取得することができます。最初はどのような値が Props に含まれているかわからないので console.log を利用して props の値を取得します。
const Page = (props) => {
console.log(props);
return <div className="m-4 font-bold">Blog ID:</div>;
};
export default Page;
ブラウザから/blog/100 にアクセスするとブラウザのデベロッパーツールのコンソールではなく開発サーバを起動したターミナルに props の値が表示されます。ターミナルに表示されることから page ファイルのコードがサーバ側で実行されていることがわかります。
{ params: { id: '100' }, searchParams: {} }
id はディレクトリ名に設定した名前と一致し[slug]という名前にした場合は id ではなく slug プロパティとして保存されます。
props に含まれるオブジェクトがわかったので TypeScript を利用している場合は Props の型を指定し params に含まれる id をブラウザ上に表示させます。
const Page = ({ params }: { params: { id: string } }) => {
return <div className="m-4 font-bold">Blog ID: {params.id}</div>;
};
export default Page;
ブラウザ上に id の値を表示
/blog/以下の値を変更するとその値がブラウザに表示されます。1, 100 などの数値ではなく what-is-nextjs などの文字列でも表示されます。
ここでは URL/blog/以下の値を取り出し表示させるだけでしたが実際のアプリケーションではこの値を利用してデータベースにアクセスしてレコードを取得したり、さらに別のサーバにアクセスを行いデータを取得してページを表示するといった設定を行います。
catch-all-segments の設定
/blog/1, /blog/2, ..., /blog/100 でアクセスを行うことができましたが/blog/1/2/3 でアクセスが行われた場合にはどのような方法で対応するのか確認していきます。
/blog/1/2/3 でアクセスした場合に props の params どのような値が含まれるか console.log を利用して params の中身を確認します。
const Page = ({ params }) => {
console.log(params);
return <div className="m-4 font-bold">Blog ID: </div>;
};
export default Page;
ブラウザから/blog/1/2/3 にアクセスすると 404 ページが表示されるため params の値を確認することはできません。
Not Found ページの表示
1,2,3 の値を取得するためにはディレクトリ名を[id]から[...id]に変更します。設定後、再度ブラウザからアクセスすると配列の形で 1,2,3 の値を取得することができます。
{
id: ['1', '2', '3'];
}
params の型も下記のように配列で設定します。
const Page = ({ params }: { params: { id: string[] } }) => {
console.log(params);
return <div className="m-4 font-bold">Blog ID: </div>;
};
export default Page;
こちらは通常の設定方法ですが、/blog/1/2/3 でページを表示させるためには[id]ディレクトリの下に[userId]、さらにその下に[categoryId]を作成して[categoryId]の下に page.tsx ファイルを作成します。useId と catagoryId は任意の名前をつけることができますが id, userId, categoryId と異なる名前をつけてください。同じ名前をつけた場合には”Error: You cannot have the same slug name "id" repeat within a single dynamic path”のメッセージが表示されます。
const Page = ({ params }: { params: { id: string[] } }) => {
console.log(params);
return <div className="m-4 font-bold">Blog ID: </div>;
};
export default Page;
ターミナルにはオブジェクトとして下記のように表示されます。
{ id: '1', userId: '2', categoryId: '3' }
params の型は下記のように設定します。
const Page = ({
params,
}: {
params: { id: string, userId: string, categoryId: string },
}) => {
console.log(params);
return <div className="m-4 font-bold">Blog ID: </div>;
};
export default Page;
Link の設定
Link コンポーネントを利用することでページ間をスムーズに移動することができます。about ページから/(ルート)ページに移動できるように Link コンポーネントを利用して設定を行います。Link コンポーネントを利用するためには next/link の import が必要となります。Link コンポーネントでは href props に移動先のページの URL を設定します。
import Link from 'next/link';
const Page = () => {
return (
<div className="flex flex-col items-center">
<Link href="/" className="underline">
Home
</Link>
<h1 className="text-2xl">About</h1>
</div>
);
};
export default Page;
about ページにアクセスを行い、表示されている Home の文字列をクリックすると/(ルート)へ移動します。
Link コンポーネントの設定
app ディレクトリの page.tsx ファイルにも Link コンポーネントを設定して about ページへ移動できるように設定を行っておきます。
import Link from 'next/link';
export default function Home() {
return (
<div className="m-4">
<Link href="/about" className="underline">
About
</Link>
<h1 className="text-2xl">Home</h1>
</div>
);
}
About のリンクをクリックすると about ページに移動できません。
ルートページから about ページへのリンク設定
prefetch の設定
Link コンポーネントではデフォルトから prefetch の機能が設定されています。開発環境ではリンクにカーソルを当てるとリンク先のページに関する JavaScript ファイルなどがバックグランドで自動でダウンロードされます。本番環境では Viewport に入っているリンク先のファイルがバックグランドで自動でダウンロードされます。
prefect 機能を利用したくないという場合は Link コンポーネントの prefetch props を false にすることで無効化できます。
<Link href="/about" className="underline" prefetch={false}>
Parallel Routes の設定
Parallel Routing を利用することで 1 つの Layout で複数の Page コンポーネントを表示させることができます。
app ディレクトリの下に@analytics, @team の 2 つのディレクトリを作成します。Route Group では()を利用しましたが Parallel Routing では@をディレクトリの先頭につけることでルーティングのパスに影響されない Page コンポーネントとなります。それぞれのディレクトリの下に page.tsx ファイルを作成して以下のコードを記述します。
const Page = () => {
return (
<div className="m-4">
<h1 className="text-2xl">Analytics</h1>
</div>
);
};
export default Page;
const Page = () => {
return (
<div className="m-4">
<h1 className="text-2xl">Team</h1>
</div>
);
};
export default Page;
追加した Page コンポーネントは app ディレクトリ直下の layout.tsx ファイルで設定を行います。children と同様に team と analytics を props で設定します。
import './globals.css';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
team,
analytics,
}: {
children: React.ReactNode,
team: React.ReactNode,
analytics: React.ReactNode,
}) {
return (
<html lang="en">
<body className={inter.className}>
<div>{children}</div>
<div>{team}</div>
<div>{analytics}</div>
</body>
</html>
);
}
以上で設定は完了です。
/(ルート)にアクセスすると childret に対応する app/page.tsx と@analytics, @team の page.tsx ファイルの内容がブラウザ上に表示されます。
複数の Page コンポーネントを表示
About ページのリンクをクリックしても引き続き複数の Page コンポーネントがブラウザ上に表示されます。
About ページでも複数の Page コンポーネント表示
ここまでは設定通りに動作しましたが/about ページでリロードを行ってください。リロードを行うと 404 ページが表示されます。
404 ページの表示
Link コンポーネントによるページ移動(Soft Navigation)では設定通りに動作しますがリロードのように直接ページにアクセスするような場合(Hard Navigation)には設定通りには動作しません。
404 ページを表示させないためには@analytics, @team ディレクトリの下に defalut.tsx ファイルを作成する必要があります。
export default function Default() {
return <div className="m-4">Analytics Page</div>;
}
export default function Default() {
return <div className="m-4">Team Page</div>;
}
defalut.tsx ファイルを作成後、/about のページでリロードを行うと default.tsx ファイルに記述した内容が表示されます。
default.tsx ファイルの設定後
@analytics ディレクトリの default.tsx ファイルでのみ同じディレクトリにある page.tsx を import して表示されるか確認を行います。
import Page from './page';
export default function Default() {
return <Page />;
}
\import した Page コンポーネントが表示されることが確認できます。
Page コンポーネントの import
Loading の設定
外部からのデータ取得
外部のデータリソースから fetch 関数を利用してデータを取得するために無料で利用することができる JSONPlaceHolder を使います。JSONPlaceHolder が提供する URL にアクセスすると JSON データを取得することができるので開発など外部のリソースを利用した動作確認に活用できます。
users ページを作成してユーザ一覧を表示させるために app ディレクトリの下に users ディレクトリを作成して page.tsx ファイルを作成します。
type User = {
id: string,
name: string,
email: string,
};
const Page = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users: User[] = await response.json();
console.log(users);
return (
<div className="m-4">
<h1 className="text-lg font-bold">ユーザ一覧</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
export default Page;
App Router を利用した場合デフォルトですべてのコンポーネントは Server Components として動作するためサーバ側ですべての処理が行われます。Server Components ではサーバ側で fetch 関数が実行されるため console.log を利用した場合は開発サーバを実行しているターミナルに取得した情報が表示されます。クライアント(ブラウザ)で fetch 関数を実行するた場合はブラウザのデベロッパーツールのコンソールには取得したデータが表示されますが Server Components の場合には表示されません。
ブラウザ側ではサーバで処理が完了したデータを受け取り描写することが確認できます。
ユーザ一覧の表示
Loading 設定の動作確認のため、about ページから users ページを移動できるように about ページのリンク先を Home から User に変更します。
import Link from 'next/link';
const Page = () => {
return (
<div className="flex flex-col items-center">
<Link href="/users" className="underline">
User
</Link>
<h1 className="text-2xl">About</h1>
</div>
);
};
export default Page;
about ページからリンクを利用して user ページに移動できるようになりました。
遅延処理の追加
Server Components ではサーバ側ですべての処理が行われるためデータの取得に時間がかかっている場合にどのような動作になるのか理解しておく必要があります。動作を確認するため Promise と setTimeout を利用して意図的に遅延を作ります。遅延の処理を追加する前に about ページからの users ページへのリンクをクリックすると即座にユーザ一覧が表示されることを確認しておきます。
users ディレクトリの page.tsx ファイルで fetch 関数を実行する前に 5 秒間の遅延を追加します。
const Page = async () => {
await new Promise((resolve) => setTimeout(resolve, 5000));
const response = await fetch('https://jsonplaceholder.typicode.com/users');
遅延処理を追加後に about ページの users ページへのリンクをクリックします。クリックして 5 秒間何も画面に変化はありません。5 秒経過するとユーザ一覧が表示されます。このことから Server Component での処理が完了するまでページが表示されないことがわかりました。
loading.tsx ファイルの設定
処理が完了するまでページ上で何も変化がないのはユーザにとって気持ちのいいものではなく離脱につながります。ページが表示されない問題を解決するためにサーバ側での処理中にブラウザ上に現在データのローディング中であることを伝えるメッセージを表示させるため users ディレクトリに loading.tsx ファイルを作成します。
export default function Loading() {
return (
<div className="flex justify-center items-center h-screen font-bold">
ローディング中
</div>
);
}
about ページの users ページへのリンクをクリックすると loading.tsx に設定した内容がブラウザ上に表示されます。loading.tsx ファイルを設定することで現在データを取得中であることがわかるようになりました。
loading のメッセージの表示
UserList コンポーネントの作成
手動で行う Loading 設定の準備として users/page.tsx ファイルからユーザ一覧の処理部分を取り出すため users ディレクトリに UserList.tsx ファイルを作成します。
type User = {
id: string,
name: string,
email: string,
};
const UserList = async () => {
await new Promise((resolve) => setTimeout(resolve, 5000));
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users: User[] = await response.json();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
export default UserList;
作成した UserList コンポーネントを users/page.tsx ファイルで import します。
import UserList from './UserList';
const Page = async () => {
return (
<div className="m-4">
<h1 className="text-lg font-bold">ユーザ一覧</h1>
<UserList />
</div>
);
};
export default Page;
page.tsx ファイルの UserList タグの箇所に TypeScript に関するメッセージがに表示されます。エラーの表示される原因は UserList コンポーネントが async を利用した非同期の Server Component のためです。async を利用していないコンポーネントの場合にメッセージは表示されません。
TypeScript のメッセージ
メッセージの表示を止めるためドキュメントに記載されている内容を元に一時的な対応策として”{/* @ts-expect-error Async Server Component */}
"のコメントを UserList コンポーネントの上に追加します。追加すると TypeScript に関するメッセージは表示されなくなります。
import UserList from './UserList';
const Page = async () => {
return (
<div className="m-4">
<h1 className="text-lg font-bold">ユーザ一覧</h1>
{/* @ts-expect-error Async Server Component */}
<UserList />
</div>
);
};
export default Page;
動作確認を行うと UserList コンポーネントを作成する前と変わらず about ページから User ページに移動するとブラウザ上に"ローディング中"の文字が表示されます。
page.tsx ファイルはデフォルトで Sever Component として動作しますが UserList.tsx ファイルもどのようにデフォルトで Server Component として動作します。
手動での Suspense の設定
loading.tsx ファイルを作成することでサーバでの処理中に Loading のメッセージが表示されてるようになりましたがこれは React が持つ Suspense の機能を利用して行われています。Next.js が Suspense の設定を自動で行ってくれるため Suspense が利用されていることを意識することはありませんが loading.tsx ファイルを利用せず Suspense を明示的に利用して設定を行うことができます。
users ディレクトリに作成した loading.tsx ファイルを削除するか別名で保存してください。
次に UserList タグを Suspense タグでラップして fallback props にサーバ処理中にブラウザ上に表示させたいメッセージを設定します。Suspense は react から import します。
import { Suspense } from 'react';
import UserList from './UserList';
const Page = async () => {
return (
<div className="m-4">
<h1 className="text-lg font-bold">ユーザ一覧</h1>
<Suspense fallback={<p>Loading...</p>}>
{/* @ts-expect-error Async Server Component */}
<UserList />
</Suspense>
</div>
);
};
export default Page;
Suspense の設定後、about ページから users ページに移動します。先程とは異なりユーザ一覧の文字列は即座に表示され Suspense でラップした UserList コンポーネントの箇所にのみ"Loading..."の文字が表示されます。
ユーザ一覧の文字は表示
loading.tsx を利用した場合にはページ全体に対して自動で Suspense が設定されているのでページ内のいずれかのコンポーネントのデータ取得処理が行われている場合はページ全体に対して loading.tsx ファイルの内容が表示されます。Suspense をコンポーネント単位でラップすることでそのコンポーネントに対する Loading 設定を行うことができます。
別々の Suspese タグでラップした複数のコンポーネントを 1 つのページに設定して異なる時間で遅延を行った場合にどのような動作になるか確認します。users ディレクトリに OtherUserList.tsx ファイルを作成して UserList.tsx ファイルの内容をコピーして遅延の時間のみ変更を行います。遅延時間を 2 秒に設定しています。
type User = {
id: string,
name: string,
email: string,
};
const OtherUserList = async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users: User[] = await response.json();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
export default OtherUserList;
users/page.tsx ファイルに作成した OtherUserList コンポーネントを追加します。UserList と OtherUseList コンポーネントには別々の Suspense タグを設定して、fallback のメッセージの内容を変更して UserList の fallback には文字にカラーを設定しています。
import { Suspense } from 'react';
import UserList from './UserList';
import OtherUserList from './OtherUserList';
const Page = async () => {
return (
<div className="m-4">
<h1 className="text-lg font-bold">ユーザ一覧</h1>
<Suspense fallback={<p className="text-red-700">Loading UserList...</p>}>
{/* @ts-expect-error Async Server Component */}
<UserList />
</Suspense>
<Suspense fallback={<p>Loading OtherUserList...</p>}>
{/* @ts-expect-error Async Server Component */}
<OtherUserList />
</Suspense>
</div>
);
};
export default Page;
about ページから users ページに移動直後はどちらもサーバ上で処理が行われているため fallback のメッセージが表示されます。
2 つのコンポーネントがサーバ上で処理中
2 秒経過すると OtherUserList のサーバ上での処理が完了してユーザ一覧が表示されます。UserList は引き続きサーバ上で処理を行っているので fallback のメッセージが表示されています。
2 秒後に 1 つのコンポーネントの処理が完了
5 秒経過すると UserList のサーバ上での処理も完了するので UserList コンポーネントの処理で取得したユーザ一覧が表示されます。
このように Suspense タグを利用することですべてのページ上の処理が完了して一括でブラウザ上に表示されるのではなく処理が完了したコンポーネント毎にサーバからデータを受け取りブラウザ上に表示させることができます。この機能を Streaming と呼びます。
Error Handling
users/page.tsx ファイルと UserList.tsx ファイルを利用して Error Handling の動作確認を行います。
users/page.tsx ファイルでは UserList コンポーネントを import しています。
import UserList from './UserList';
const Page = async () => {
return (
<div className="m-4">
<h1 className="text-lg font-bold">ユーザ一覧</h1>
{/* @ts-expect-error Async Server Component */}
<UserList />
</div>
);
};
export default Page;
UserList.tsx ファイルでは fetch 関数によるデータ処理に失敗した場合にエラーを throw させるため response.ok を使って分岐を行います。fetch 関数の処理に失敗した場合に response オブジェクトの ok プロパティには false が入ります。
type User = {
id: string,
name: string,
email: string,
};
const UserList = async () => {
// await new Promise((resolve) => setTimeout(resolve, 5000));
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) throw new Error('Failed to fetch data');
const users: User[] = await response.json();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
export default UserList;
fetch 関数の引数で設定している URL を存在しないhttps://jsonplaceholder.typicode.com/userに変更します。"users"から"s"を削除して"user"としています。存在しないURLにアクセスを行うとresponse.okの値がfalseになるためErrorがthrowされます。
User ページ(/users)にブラウザからアクセスするとエラーにより Unhandled Runtime Error 画面が表示されます。
Unhandled Rutime Error の画面が表示
右上に表示されている"X"ボタンをクリックすると画面の左下にボックスが表示され再度"X"をクリックすると先程表示されていた"Unhandled Runtime Error"の画面が表示されます。
エラー画面
Unhandled Runtime Error 画面ではなくエラーメッセージのみを表示させるように Error Handling の設定を行なっていきます。
users ディレクトリに error.tsx ファイルを作成して以下のコードを記述します。
'use client';
export default function Error({ error }: { error: Error }) {
return (
<div className="m-4 font-bold">
<p>{error.message}</p>
</div>
);
}
Page ディレクトリの直下に error.tsx ファイルを作成すると Next.js が自動で Unhandled Runtime Error が発生した場合にエラーをハンドリングするため error.ts ファイルが利用されます。エラーハンドリングには React の ErrorBoundary を利用しており ErrorBoundary タグで page.tsx をラップする形で設定が行われます。
App Router ではデフォルトではすべてのコンポーネントが Server Component でサーバ側で処理が行われます。error.ts ファイルはクライアント(ブラウザ)側で処理を行う必要があるためファイルの先頭に"use client"を追加する必要があります。"use client"を明示的に設定することでコンポーネントが Server Component から Client Component に変わります。
error.tsx ファイルを設定後に users にアクセスすると error.tsx ファイルで設定したメッセージがブラウザ上に表示されます。左下に表示されているボックスの"X"をクリックすると Unhandled Runtime Error の画面が表示されます。
エラーメッセージの表示
エラー画面からの復帰
ここでの設定では存在しない URL を設定しているので何度/users にアクセスしてもエラーが発生します。しかし本番では一時的にエラーが発生しているため再度/users にアクセスするとエラーが解消に正常に動作することもあります。エラーが解消した場合にエラー画面から復帰するために reset 関数が準備されているので reset 関数の処理を error.tsx ファイルに追加します。
'use client';
export default function Error({
error,
reset,
}: {
error: Error,
reset: () => void,
}) {
return (
<div className="m-4 font-bold">
<p>{error.message}</p>
<button
className="px-2 py-1 text-white bg-blue-500 rounded-lg"
onClick={() => reset()}
>
Try again
</button>
</div>
);
}
/users にアクセスするとエラーメッセージと"Try again"ボタンが表示されます。
Try again ボタンの表示
"Try again"ボタンをクリックしても現在の設定では引き続きエラーが発生するので同じ画面が再表示されます。
Client Component
App Router ではすべてのコンポーネントはデフォルトで Server Component です。クライアント(ブラウザ)側でインタラクティブな操作を行うためには Client Componet として設定を行う必要があります。
ユーザがボタンをクリックするとカウンターの数字が増える Counter コンポーネントを利用して Client Component について確認していきます。
Counter コンポーネントの作成
app ディレクトリの直下に Counter.tsx ファイルを作成して以下のコードを記述します。これまでのコンポーネントとは異なり useState Hook を利用しています。ボタンをクリックすると useState で定義した状態 count の値が増えるだけのシンプルなコードです。
import { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState < number > 0;
const increment = () => {
setCount((prev) => prev + 1);
};
return (
<>
<div>Count: {count}</div>
<button
onClick={increment}
className="px-2 py-1 rounded-lg bg-blue-600 text-white"
>
Increment
</button>
</>
);
};
export default Counter;
作成した Counter コンポーネントを app ディレクトリ直下にある page.tsx ファイルから import します。
import Link from 'next/link';
import Counter from './Counter';
export default function Home() {
return (
<div className="m-4">
<Link href="/about" className="underline">
About
</Link>
<h1 className="text-2xl">Home</h1>
<Counter />
</div>
);
}
Failed to compile のエラー画面が表示されます。エラーの原因も丁寧に表示されています。”You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.”
Failed to compile エラー
デフォルトでは Server Components として動作するため useState Hook を利用するためには'use client’の設定を行い Client Component として動作させる必要があります。
指摘通り、Counter.tsx ファイルの先頭に'use client'の1行を追加します。'use client'は import 文よりも前のファイルの先頭に記述する必要があります。
'use clinet'を設定したことでエラーが解消されブラウザ上にはカウンターが表示され"increment"ボタンをクリックすると Count の値が増えていきます。
Counter の表示
Client Component
'use client'を追加することで Server Component から Client Component になりクライアント(ブラウザ)側で useState Hook を利用した処理が行えるようになりました。
Counter.tsx ファイルの親コンポーネントである app/page.tsx ファイルに'use client’を設定しても動作するか確認します。Counter.tsx の'use client'は削除しておきます。
'use client';
import Link from 'next/link';
import Counter from './Counter';
export default function Home() {
//略
親コンポーネントに'use client'の設定を行なっても Counter は動作します。Page コンポーネントに含まれているコンポーネントが Client Component の場合は親コンポーネントに'use client'に設定することで各コンポーネントで'use client'を設定する必要がなくなります。
動作確認が完了したら page.tsx から'use client'を削除して Counter.tsx ファイルの先頭に'use client'を戻してください。
Server Component を Client Component で利用
Server Component を Client Component 内で利用したい場合には props を利用します。
Server Component として users ディレクトリに作成済みの UserLst.tsx ファイルを利用します。サーバ側でのみ実行されるか確認するために console.log を設定しています。
type User = {
id: string,
name: string,
email: string,
};
const UserList = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) throw new Error('Failed to fetch data');
const users: User[] = await response.json();
console.log(users);
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
export default UserList;
Counter.tsx ファイルでは Server Component を受け取れるように children props を設定します。
'use client';
import { useState } from 'react';
const Counter = ({ children }: { children: React.ReactNode }) => {
const [count, setCount] = useState < number > 0;
const increment = () => {
setCount((prev) => prev + 1);
};
return (
<>
<div>Count: {count}</div>
<button
onClick={increment}
className="px-2 py-1 rounded-lg bg-blue-600 text-white"
>
Increment
</button>
{children}
</>
);
};
export default Counter;
最後に app ディレクトリの page.tsx ファイルで UserList コンポーネントを import して Counter タグの間に挿入します。
import Link from 'next/link';
import Counter from './Counter';
import UserList from './users/UserList';
export default function Home() {
return (
<div className="m-4">
<Link href="/about" className="underline">
About
</Link>
<h1 className="text-2xl">Home</h1>
<Counter>
<h2 className="font-bold text-lg mt-4">ユーザ一覧</h2>
{/* @ts-expect-error Async Server Component */}
<UserList />
</Counter>
</div>
);
}
console.log の内容は開発サーバを起動したターミナルに表示されることが確認でき props を利用することで Client Component の中で Server Component が利用できることが確認できました。
Client Component 内で Server Componet を利用
Client Component の pre-rendered
Next.js のドキュメントには”Client components are pre-rendered on the server as HTML to produce a faster initial page load”(最初のページ読み込みを高速化するために、サーバー上で HTML としてプリレンダリングされます。)と記載されているので動作確認を行います。
/(ルート)のページにアクセスを行い最初のページ読み込みを行うためリロードします。リロード後に Client Componet が事前にプリレンダリングされているか確認するためネットワークタブを確認します。
ブラウザ側ではサーバからの Response としてプリレンダリングされた HTML として受け取っていることが確認できます。
pre-rendered されたページの確認
Server Components vs Client Components
Server Components と Client Components の使い分けについてはNext.js の公式ドキュメントに掲載されているので参考にしてください。
When to use Server and Client Components?
Fetch data, バックエンドリソース(データベースなど)への直接なアクセス, アクセストークンや API キーの利用、大きさサイズのパッケージを利用する処理については Server Components、onClick などユーザとのインタラクティブがあるもの、useState などの React Hook, ブラウザの API などについては Cliet Components を利用することを推奨しています。Fetch data などはクライアントでも行えますし、アクセストークンなども利用できますがアクセストークンをクライアントコンポーネント内で利用するとアクセストークンの中身がユーザから閲覧できてしまうの Server Components を利用することになります。
Server Component のみで利用するパッケージはブラウザ側でダウンロードされることがないので JavaScript のバンドルサイズを小さくすることができます。そのため大きなサイズのパッケージは Server Component で利用することが推奨されているので Keep large dependencies on the server は Server Component 側にチェックが入っています。大きなサイズのパッケージが Client Component で利用できないわけではありませんがダウンロードする JavaScript のバンドルが大きくなのでパフォーマンスへの影響が出てきます。
Context
コンポーネント間でデータを共有したい場合に Context を利用することができます。先ほど作成した Counter の処理を Context を利用して書き換えます。
Context は Client Component でしか利用することはできません。そのため Client Component の中で定義する必要があります。
プロジェクトディレクトリの直下に context ディレクトリを作成して ConterProvider.tsx ファイルを作成します。Client Component の設定を行うためファイルの先頭には'use client'を設定します。それ以外については通常の TypeScript のコードと違いはありません。
'use client';
import React from 'react';
const CounterContext = React.createContext<
[number, React.Dispatch<React.SetStateAction<number>>] | undefined
>(undefined);
export function CounterProvider({ children }: { children: React.ReactNode }) {
const [count, setCount] = React.useState(0);
return (
<CounterContext.Provider value={[count, setCount]}>
{children}
</CounterContext.Provider>
);
}
export function useCounter() {
const context = React.useContext(CounterContext);
if (context === undefined) {
throw new Error('useCounter must be used within a CounterProvider');
}
return context;
}
作成した ConterProvider を app ディレクトリ直下の layout.tsx ファイルで import します。
import './globals.css';
import { Inter } from 'next/font/google';
import { CounterProvider } from './context/CounterProvider';
const intr = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode,
}) {
return (
<html lang="en">
<body className={intr.className}>
<CounterProvider>{children}</CounterProvider>
</body>
</html>
);
}
Context の設定は完了です。RootLayout で Context を行ったので app 下の Client Component であれば Context を利用することができます。ここでは app ディレクトリ直下に作成済みの Counter.tsx ファイルで利用します。Client Compnent でしか利用できないため'use client'をファイルの先頭に記述しています。ConterProvider から useCounter 関数を import することで count, setCount をコード内で利用することができます。
'use client';
import { useCounter } from './context/CounterProvider';
const Counter = ({ children }: { children: React.ReactNode }) => {
const [count, setCount] = useCounter();
const increment = () => {
setCount((prev) => prev + 1);
};
return (
<>
<div>Count: {count}</div>
<button
onClick={increment}
className="px-2 py-1 rounded-lg bg-blue-600 text-white"
>
Increment
</button>
{children}
</>
);
};
export default Counter;
表示される内容は Context を利用する前と変わりませんが increment ボタンをクリックすると Count の数が増えます。
Context を利用して Count の値を increment
Route Handlers
App Router の Route Handlers は Pages Router の API Routes と同等の機能です。Route Handlers を利用することで GET, POST などの HTTP メソッドを利用してアクセスすることで API エンドポイントを設定するができます。Route Handlers も
app ディレクトリの中に任意の名前のディレクトリを作成します。ここでは api という名前のディレクトリを作成します。Page コンポーネントの名前が page.tsx で決められているように Route Handlers では route.js または route.ts という名前をつける必要があります。
GET リクエストの動作確認
api ディレクトリに route.ts ファイルを作成して以下のコードを記述します。
import { NextResponse } from 'next/server';
export function GET() {
return NextResponse.json({ name: 'John Doe' });
}
/api に対して GET リクエストを送信すると JSON で{"name":"John Doe"}が戻されるというもっともシンプルなコードです。
GET リクエストであればブラウザからアクセスすることで動作確認できます。
ブラウザから/api へのアクセス
Route Handlers からのデータ取得
JSONPlaceHolder からデータを取得することもできます。
import { NextResponse } from 'next/server';
export async function GET() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
return NextResponse.json(data);
}
ブラウザからもアクセスできますが UserList コンポーネントからアクセスを行ってみます。
type User = {
id: string,
name: string,
email: string,
};
const UserList = async () => {
// await new Promise((resolve) => setTimeout(resolve, 5000));
const response = await fetch('http://localhost:3000/api');
if (!response.ok) throw new Error('Failed to fetch data');
const users: User[] = await response.json();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
export default UserList;
ブラウザから/users にアクセスすると Route Hadlers を経由してデータの取得が行われ、ユーザ一覧が表示されます。
URL パラメータの取得
検索など URL パラメータを利用した場合の URL パラメータの取得方法を確認します。
UserLists.tsx ファイルでは fetch 関数で指定する URL にパラメータを追加します。App Router で fetch 関数は Web API の fetch 関数を拡張しているためオプションを設定することができます。デフォルトでは一度 fetch 関数が実行されるとキャッシュされるためその後 fetch 関数を実行するとキャッシュしたデータが利用されるためリクエストが行われません。ここでは動作確認のまたキャッシュ機能を無効にします。
const response = await fetch('http://localhost:3000/api?name=John', {
cache: 'no-store',
});
api/route.ts では URL パラメータを取得するため request オブジェクトを利用します。request オブジェクトにはさまざまな情報が含まれていますが URL パラメータは request.url を利用して取得します。
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const name = searchParams.get('name');
console.log(name);
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
return NextResponse.json(data);
}
ブラウザから/users にアクセスすると開発サーバを起動したターミナルに"John"が表示されます。URL パラメータを取得することができるようになりました。
headers, cookies 関数
headers, cookies 関数を利用することでそれらの情報を取得することができます。
import { NextResponse } from 'next/server';
import { headers, cookies } from 'next/headers';
export async function GET() {
const headersList = headers();
const cookieStore = cookies();
console.log('headersList', headersList);
console.log('cookieStore', cookieStore);
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
return NextResponse.json(data);
}
Dynamic Routes の設定
api のように静的な API のエンドポイントではなく api/1, api/2,...api/100 などのように URL が動的に変わる場合の設定方法を確認します。
api ディレクトリの下に[]で囲んだ[id]ディレクトリを作成してその下に route.ts ファイルを作成します。
import { NextResponse } from 'next/server';
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const id = params.id;
return NextResponse.json(id);
}
ブラウザから/api/100 にアクセスすると"100"が戻されます。
実際のアプリケーションではこの値を利用してデータベースにアクセスしてレコードを取得したり、さらに別のサーバにアクセスを行いデータを取得してページを表示するといった設定を行います。
POST の設定
POST リクエストによって送信されてきたデータを取得する方法を確認します。
api/route.ts ファイルで POST リクエストで送信されてきたデータを取り出すためのコードを追加します。関数の名前は POST となります。request.json()で取得したデータをそのままクライアントに戻しています。通常はデータベースなどへのデータの挿入などを行います。
import { NextResponse } from 'next/server';
export async function GET() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
return NextResponse.json(data);
}
export async function POST(request: Request) {
const res = await request.json();
return NextResponse.json({ res });
}
POST リクエストを送信するためには入力フォームを作成する必要がありますがここでは fetch 関数を利用して POST リクエストでデータを送信するコードのみ users/page.tsx ファイルに追加します。
import UserList from './UserList';
const Page = async () => {
const response = await fetch('http://localhost:3000/api', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'John',
email: 'john@example.com',
}),
});
const data = await response.json();
console.log(data);
return (
<div className="m-4">
<h1 className="text-lg font-bold">ユーザ一覧</h1>
{/* @ts-expect-error Async Server Component */}
<UserList />
</div>
);
};
export default Page;
/users にアクセスを行い、body プロパティに送信したオブジェクトの中身が開発サーバを起動しているターミナルに表示されれば Route Handlers で正しく POST リクエストを受け取り、受け取ったデータをブラウザに戻していることになります。
以下のように表示されれば正しく動作しています。
{ res: { name: 'John', email: 'john@example.com' } }
実際のアプリケーションでは POST リクエストから送信されてきたデータをデータベースに登録するといった処理を行います。
Fetching
これまで触れてきませんでしたが fetch 関数を実行すると開発サーバを起動したターミナルに下記のようなメッセージが表示されていました。
- ┌ GET /users 200 in 610ms
│
└──── GET http://localhost:3000/api 200 in 450ms (cache: MISS)
注目したいところは cache の値で上記では MISS と表示されています。これは cache にデータが存在しないためデータを取得するために fetch 関数を実行しています。その後再度ページを開くと cache の値が HIT になっています。これは cache の値を利用したので fetch 関数を実行されません。
- ┌ GET /users 200 in 116ms
│
└──── GET http://localhost:3000/api 200 in 2ms (cache: HIT)
このようにデフォルトの設定では cache を利用するような設定になっています。
GET の行にはアクセスのあった URL とステータスコード、経過した時間が表示されています。cache が HIT した場合には時間がかなり短くなっていることがわかります。
fetch 関数の cache オプション
このように App Router で fetch 関数を利用すると自動で cache を利用する設定になっています。これは Web API の fetch 関数を拡張しているためでオプションを利用してキャッシュの設定を変更することができます。
デフォルトでは force-cache が設定されています。force-cache のほかに cache を無効にする no-store があります。
どちらも値の名前でどのような設定か想像できると思いますが cache プロパティの値に no-store を設定します。
const response = await fetch('http://localhost:3000/api', {
cache: 'no-store',
});
設定後は何度/users にアクセスしても cache の値が"MISS"のままです。
- ┌ GET /users 200 in 206ms
│
└──── GET http://localhost:3000/api 200 in 170ms (cache: MISS)
http://localhost:3000/api に GET リクエストを送信しているので実際にリクエストが送信されているのか確認するために console.log を設定します。
export async function GET() {
console.log('GET Request');
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
return NextResponse.json(data);
}
設定後は毎回アクセスするたびに"GET Request"が表示されます。つまり api/route.ts に GET リクエストが送信されてきていることになります。
GET Request
cache の設定を削除してデフォルトの状態に戻します。削除するとデフォルトの force-cache となります。
const response = await fetch('http://localhost:3000/api');
cache の値が HIT となり、fetch 関数が実行されなくなり/api/routes.ts に GET リクエストが送信されなくなるため GET Reqeust のメッセージが表示されることはなくなりました。
- ┌ GET /users 200 in 42ms
│
└──── GET http://localhost:3000/api 200 in 1ms (cache: HIT)
fetch の next.revalidate の設定
cache オプションでは cache を利用するかどうかの設定でしたが next.revalidate オプションを設定することで cache のライフタイムを指定することができます。設定したライフタイムを経過すると fetch 関数が新たに実行されます。
revalidate の値で秒数を設定することができるので下記では 5 秒を設定しています。fetch でアクセスを行い 5 秒間の間は cache の値を利用しますがその時間を過ぎると再度 fetch 関数が行われます。
const response = await fetch('http://localhost:3000/api', {
// cache: 'no-store',
next: { revalidate: 5 },
});
Automatic Request Deduping
Automatic Request Deduping は複数のコンポーネントで同じ URL に対して同時にリクエストを送信する際にリクエストの重複をなくしてリクエストを最適化する機能です。
下記は Next.js のドキュメントに記載されているイメージ図ですが複数のコンポーネントの重複したリクエストを最適化しているのが理解できるかと思います。この機能により重複したリクエストの数を減らすことができます。
deduplicated fetch request
Database
Prisma と手軽に利用できる SQLite データベースを利用して Server Compoent から Prisma を経由して SQLite データベースにアクセスを行い、データが取得できるかを確認します。
Prisma のインストール
Prisma を設定して SQLite データベースに接続するために prisma パッケージのインストールを行います。
% npm install prisma --save-dev
Prisma の設定
Prisma 用の設定ファイルを作成するために npx prisma init コマンドを実行します。実行するとプロジェクトディレクトリ下には prisma ディレクトリと.env ファイルが作成されます。prisma ディレクトリには Prisma の設定ファイルである schema.prisma ファイルが作成されています。.env ファイルはデータベースに接続するために必要となる環境変数を設定するために利用します。SQLite データベースを利用するのでオプション--datasource-provider に sqlite を設定します。オプションを指定しない場合はデフォルトでデータベースには postgresql が設定されています。
% npx prisma init --datasource-provider sqlite
✔ Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.
Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Run prisma db pull to turn your database schema into a Prisma schema.
3. Run prisma generate to generate the Prisma Client. You can then start querying your database.
More information in our documentation:
https://pris.ly/d/getting-started
schema.prisma ファイルでは–datasource-provider を指定した実行した場合は SQLite データベースに関する設定は完了しているのでモデルの設定を行います。モデルには post テーブルを作成するためのスキーマ(テーブルの構成情報)を追加します。
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Post {
id Int @id @default(autoincrement())
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
schema.prisma ファイルでのモデルの完了したら SQLite データベースに post テーブルを作成するために npx prisma db push コマンドを実行します。
% npx prisma db push
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
SQLite database dev.db created at file:./dev.db
🚀 Your database is now in sync with your Prisma schema. Done in 16ms
Running generate... (Use --skip-generate to skip the generators)
//略
✔ Generated Prisma Client (4.14.0 | library) to ./node_modules/@prisma/client in 87ms
コマンドを実行すると.env ファイルの DATABASE_URL で指定した場所に SQLite のデータベースファイルが作成されます。schema.prisma ファイルの model 以外の設定を変更していない場合には prisma フォルダに dev.db ファイルが作成されます。
Prisma Studio からのデータベース接続
Prisma には Prisma Studio という Prisma 専用のツールを利用してデータベースにアクセスを行うことができます。Prisma Studio を起動するために npx prisma studio コマンドを実行します。
% npx prisma studio
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Prisma Studio is up on http://localhost:5555
ブラウザから http://localhost:5555 にアクセスすると Post テーブルをブラウザ上から確認することができます。
Prisma Studio からのデータベースへのアクセス
ブラウザからデータを挿入することができるので動作確認用に 2 件のデータを追加します。
2 件のデータ挿入
Prisma Client の設定
Next.js からデータベースに接続するために Prisma Client の設定を行う必要があります。設定を行うために lib ディレクトリをプロジェクトフォルダ直下に接続を行い、prisma.ts ファイルを作成します。
declare global {
var prisma: PrismaClient;
}
import { PrismaClient } from '@prisma/client';
let prisma: PrismaClient;
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient();
} else {
if (!global.prisma) {
global.prisma = new PrismaClient();
}
prisma = global.prisma;
}
export default prisma;
Next.js からデータベースに接続する場合はこのファイルから Prisma Client の import を行います。これで Prisma の設定は完了です。
データの表示
Prisma を経由して SQLite の post テーブルに保存されているデータを取得して表示するために app ディレクトリの下に post ディレクトリを作成して page.tsx ファイルを作成し以下のコードを記述します。
import prisma from "@/lib/prisma';
const Page = async () => {
const posts = await prisma.post.findMany();
return (
<div className="m-4">
<h1 className="text-lg font-bold">記事一覧</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
};
export default Page;
ブラウザから/posts にアクセスすると post テーブルに保存した title が表示されます。
post テーブルに保存されたデータの表示
page.tsx ファイルは Server Component として動作しているので page.txs ファイルにデータベースへの接続処理のコードを記述することができます。
Route Handlers を利用した場合
Route Handlers を利用して API エンドポイントを作成した場合の動作確認も行っておきます。
app ディレクトリ下に api ディレクトリを作成します。さらに posts ディレクトリを作成 route.ts ファイルを作成して以下のコードを記述します。
import { NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET() {
const posts = await prisma.post.findMany();
return NextResponse.json(posts);
}
posts/page.tsx ファイルは fetch 関数を利用して作成した/api/posts からデータを取得します。
import { Post } from '@prisma/client';
const Page = async () => {
const response = await fetch('http://localhost:3000/api/posts');
const posts: Post[] = await response.json();
return (
<div className="m-4">
<h1 className="text-lg font-bold">記事一覧</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
};
export default Page;
ブラウザから/posts にアクセスすると先ほどと同じ画面が表示されます。
post テーブルに保存されたデータの表示
データの登録
フォームを利用してデータベースにデータを登録する方法を確認します。
Next.js 13.4 で新たに"Server Actions: Mutate data on the server with zero client JavaScript"が登場しました。Server Actions については現在アルファなので今後も仕様の変更が考えられるため下記の記事で紹介しています。そのため本文書では Server Actions を利用していません。
入力フォームに 2 つの input 要素を持ち、Post モデルで定義した title と content を入力して submit ボタンをクリックすると handleSubmit 関数が実行され、Router Handlers に POST リクエストが送信されるシンプルなフォームです。ファイル名を AddPost.tsx ファイルとして posts ディレクトリの下に作成します。
import { useState } from 'react';
export default function AddPost() {
const [title, setTitle] = useState < string > '';
const [content, setContent] = useState < string > '';
const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (
event
) => {
event.preventDefault();
await fetch('http://localhost:3000/api/posts/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title, content }),
});
setTitle('');
setContent('');
};
return (
<form onSubmit={handleSubmit} className="flex flex-col space-y-4 mt-8">
<div>
<label htmlFor="title">title:</label>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
className="border"
required
/>
</div>
<div>
<label htmlFor="content">content:</label>
<input
value={content}
onChange={(e) => setContent(e.target.value)}
className="border"
required
/>
</div>
<div>
<button
type="submit"
className="px-2 py-1 bg-blue-500 text-white rounded-lg"
>
Submit
</button>
</div>
</form>
);
}
作成後、page.tsx ファイルで import を行います。
import { Post } from '@prisma/client';
import AddPost from './AddPost';
const Page = async () => {
const response = await fetch('http://localhost:3000/api/posts');
const posts: Post[] = await response.json();
return (
<div className="m-4">
<h1 className="text-lg font-bold">記事一覧</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<AddPost />
</div>
);
};
export default Page;
送信されてきた POST リクエストを受け取りデータベースにデータ登録できるように Route Handlers の設定も追加します。更新するファイルは app/api/posts/route.ts です。
import { NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET() {
const posts = await prisma.post.findMany();
return NextResponse.json(posts);
}
export async function POST(request: Request) {
const req = await request.json();
await prisma.post.create({ data: req });
return NextResponse.json(req);
}
Route Handlers の設定完了後、ブラウザから/posts にアクセスを行うと useState Hook を利用しているのでエラーメッセージが表示されます。”You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.”。デフォルトでは app ディレクトリ以下のコンポーネントは Server Component として動作するため useState Hook を利用することができません。Client Component として動作させるためファイルの先頭に'use client'を追加します。
'use client'
import { useState } from 'react';
export default function AddPost() {
//略
'use client'を追加するとエラーは解消されるので記事一覧とフォームが表示されます。
入力フォームの表示
title と content に文字列を入力して"Submit"ボタンをクリックしてください。"Submit"ボタンをクリックしても入力したデータは表示されません。Prisma Studio を利用してデータが登録されているか確認すると Post テーブルには入力したデータが登録されていることが確認できます。
ページのリロードを行っても新しいデータが表示されることはありません。理由は fetch 関数のオプションの cache の値を"no-store"に変更していないためキャッシュに保存されたデータが表示されます。開発サーバを起動したターミナルにも"cache: HIT"が確認できます。
page.tsx ファイルで fetch 関数のオプションに cache:'no-store'を追加します。
const response = await fetch('http://localhost:3000/api/posts', {
cache: 'no-store',
});
no-store を設定すると"cache: MISS"になるためキャッシュではないデータの再取得が行われます。
no-store を設定することでデータが取得できる
この状態で再度データの登録を行います。入力フォームに文字列を入力して"Submit"ボタンをクリックします。しかし画面には入力したデータは表示されません。Prisma Studio を確認すると問題なくデータは登録されています。ページをリロードすると入力したデータが再表示されるようになります。
"Submit"ボタンを押してもページのリフレッシュが行われないためデータの再取得が行われないことが原因です。登録が完了した後にページのリフレッシュが行えるように useRouter の refresh メソッドを利用します。useRouter は next/navigation から import します。
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function AddPost() {
const router = useRouter();
const [title, setTitle] = useState<string>('');
const [content, setContent] = useState<string>('');
const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (
event
) => {
event.preventDefault();
await fetch('http://localhost:3000/api/posts/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title, content }),
});
setTitle('');
setContent('');
router.refresh();
};
//略
useRouter.refresh を追加後に入力フォームから入力後に"submit"ボタンをクリックすると入力したデータが自動で反映されます。
App Router の環境でのデータの登録方法を確認できました。
Optimizing
Metadata の設定
head タグに中に挿入する title や description などの Meta タグを設定する方法には Static と Dynamic な 2 つの設定方法が提供されています。設定は layout.ts または page.ts ファイルで行います。
static な設定方法は app ディレクトリの layout.tsx ファイルにデフォルトから設定されています。
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
この設定が行われているため/users にアクセスしてもブラウザのタブに設定した title が表示されています。
ブラウザのタブに表示されている title の確認
Static Metadata の設定
ユーザページに表示される title を変更したい場合には下記のように設定を行うことができます。
export const metadata = {
title: 'ユーザの一覧ページ',
description: 'JSONPlaceHolderから取得したユーザ一覧です。',
};
設定した値がブラウザのタブに反映されます。description についてはページのソースで確認できます。
page.tsx ファイルで title と description の設定
Dynamic Metadata の設定
Dynamic Metadata を設定するためユーザ一覧に表示したユーザ名をクリックすると詳細ページに移動できるように Dynamic Routes を利用して設定を行います。リンクには Link コンポーネントを利用しています。
import UserList from './UserList';
const Page = async () => {
return (
<div className="m-4">
<h1 className="text-lg font-bold">ユーザ一覧</h1>
{/* @ts-expect-error Async Server Component */}
<UserList />
</div>
);
};
export default Page;
import Link from 'next/link';
export type User = {
id: string,
name: string,
email: string,
};
const UserList = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users: User[] = await response.json();
return (
<ul>
{users.map((user) => (
<li key={user.id}>
<Link href={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
);
};
export default UserList;
現在の設定ではユーザ名をクリックすると Dynamic Routes の設定を行なっていないので 404 ページが表示されます。
Dynamic Routes を設定するため users ディレクトリに[id]ディレクトリを作成してその下に page.tsx ファイルを作成します。
import { type User } from '../UserList';
const Page = async ({ params }: { params: { id: string } }) => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${params.id}`
);
const user: User = await response.json();
return (
<div className="m-4">
<h1 className="text-lg font-bold">ユーザ詳細</h1>
<p>ID: {user.id}</p>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
);
};
export default Page;
設定後、ユーザ名をクリックするとユーザの詳細画面が表示されます。
ユーザの詳細画面
title にユーザ名を設定したい場合はページの内容が動的に変わるため Static な方法で Metadata を設定することができません。そのため generateMetadata 関数を利用して Dynamic に Metadata を設定します。
title を設定する際にも fetch 関数を利用してデータを利用するため getUser 関数を作成しています。画面に表示されるユーザ情報と Metadata に利用するユーザ情報のため 1 つの Page コンポーネントの中で同じ URL に対して fetch の処理を行っていますが Automatic Request Deduping により同じ URL へのリクエストは最適化されます。
import type { Metadata } from 'next';
import { type User } from '../UserList';
async function getUser(id: string) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`
);
return response.json();
}
export async function generateMetadata({
params,
}: {
params: { id: string },
}): Promise<Metadata> {
const user = await getUser(params.id);
return { title: user.name };
}
const Page = async ({ params }: { params: { id: string } }) => {
const user: User = await getUser(params.id);
return (
<div className="m-4">
<h1 className="text-lg font-bold">ユーザ詳細</h1>
<p>ID: {user.id}</p>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
);
};
export default Page;
ブラウザから確認すると Dynamic に title が設定されていることがわかります。
Dynamic に設定を行った title の確認
全ページのタイトルにアプリケーションの名前等(XXXX | Next App)を設定したい場合があります。その場合は layout.tsx ファイルの metada の設定で template を利用することができます。
export const metadata = {
title: {
default: 'Create Next App',
template: `%s | Next App`,
},
description: 'Generated by create next app',
};
ブラウザで確認するとページのタイトルの後に"| App"が追加されています。
タイトルに | Next. App を追加
favicon の設定
デフォルトから app ディレクトリの下に favicon.ico ファイルが存在していますが layout.tsx でも favicon.ico の設定を行っていません。app ディレクトリの下に favicon.ico を配置するだけで自動で head タグの中に追加されます。もし favicon.ico の名前を変更したり削除するとタグは消えます。
<link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="any" />
opengraph-image の設定
OGP の画像を設定したい場合は opengraph-image というファイル名でサポートされている.jpg, .jpeg, .png, .gif の拡張子を持つファイルを app ディレクトリに設定するだけで自動で設定を行ってくれます。ここでは opengraph-image.png ファイルを app ディレクトリに保存しました。下記のコードが head タグに追加されます。
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta
property="og:image"
content="http://localhost:3000/opengraph-image.png?6462ecdf1a65b6d2"
/>
app ディレクトリに opengraph-image.png ファイルを保存すると/users に上記のタグが head の追加されます。opengraph-image.png ファイルを app ディレクトリから users ディレクトリに移動すると/(ルート)にアクセスしても OGP 画像の meta タグは設定されませんが/users では meta タグが表示されます。
動的な OGP 画像の作成
/users/1 にアクセスした場合に動的に OGP の画像を作成することができます。/users/[id]ディレクトリに opengrap-image.ts ファイルを作成した以下のコードを設定します。
import { ImageResponse } from 'next/server';
export const size = {
width: 1200,
height: 630,
};
export const contentType = 'image/png';
export default async function Image({ params }: { params: { id: string } }) {
const user = await fetch(
`https://jsonplaceholder.typicode.com/users/${params.id}`
).then((res) => res.json());
return new ImageResponse(
(
<div
style={{
fontSize: 48,
background: 'aqua',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{user.name}
</div>
),
{
...size,
}
);
}
ブラウザのデベロッパーツールの要素を利用して og
の meta タグを見つけます。<meta property="og:image" content="http://localhost:3000/users/1/opengraph-image?eebd5a17f61ce462">
この URL をブラウザの URL のバーに貼り付けて確認します。以下のように OGP 画像が動的に作成されることが確認できます。
Dynamic に作成される OGP 画像
Sitemap
サイトマップを設定したい場合には app ディレクトリの下に sitemap.xml ファイルを保存します。sitemap.xml ファイルの内容は Next.js のドキュメントからコピーしています。
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://acme.com</loc>
<lastmod>2023-04-06T15:02:24.021Z</lastmod>
</url>
<url>
<loc>https://acme.com/about</loc>
<lastmod>2023-04-06T15:02:24.021Z</lastmod>
</url>
<url>
<loc>https://acme.com/blog</loc>
<lastmod>2023-04-06T15:02:24.021Z</lastmod>
</url>
</urlset>
http://localhost:300/sitemap.xml にアクセスすると app ディレクトリに保存した sitemap.xml ファイルの内容が表示されます。
ブラウザから sitemap.xml の確認
静的に sitemap.xml を作成しましたが動的に作成することができます。app ディレクトリに sitemap.ts ファイルを作成して以下のコードを記述します。
import { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://acme.com',
lastModified: new Date(),
},
{
url: 'https://acme.com/about',
lastModified: new Date(),
},
{
url: 'https://acme.com/blog',
lastModified: new Date(),
},
];
}
ブラウザから http://localhost:3000/sitemap.xml にアクセスすると new Date()を設定しているのでアクセスした日が表示されます。
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://acme.com</loc>
<lastmod>2023-05-10T06:48:37.849Z</lastmod>
</url>
<url>
<loc>https://acme.com/about</loc>
<lastmod>2023-05-10T06:48:37.849Z</lastmod>
</url>
<url>
<loc>https://acme.com/blog</loc>
<lastmod>2023-05-10T06:48:37.849Z</lastmod>
</url>
</urlset>
通常はデータベースまたはリモートサーバからページ情報を取得するためここでは JSONPlaceHolder を利用して取得したデータを利用して sitemap を作成します。
import { MetadataRoute } from 'next';
type User = {
id: string,
};
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users: User[] = await response.json();
const usersUrl = users.map((user) => {
return {
url: `http://localhost:3000/users/${user.id}`,
lastModified: new Date(),
};
});
return [
{
url: 'http://localhost:3000',
lastModified: new Date(),
},
{
url: 'http://localhost:3000/users',
lastModified: new Date(),
},
...usersUrl,
];
}
ブラウザから sitemap.xml にアクセスすると以下のように表示されます。
リモートサーバから取得したデータで sitemap.xml を作成
robots.txt
robots.txt はクロール可能なページを検索エンジンからのクローラーに対して伝えるファイルです。
app ディレクトリに robots.txt ファイルを作成して保存します。/private ディレクトリに対してのみクロールを拒否する設定を行っています。User-Agent ではすべてのクローラーに設定を伝えています。
User-Agent: *
Allow: /
Disallow: /private/
Sitemap: http://localhost:3000/sitemap.xml
ファイルを app ディレクトリに保存後に http://localhost:3000/robots.txt にアクセスすると robots.txt に記述した内容が表示されます。
robots.txt ではなく robots.ts ファイルでも設定を行うことができます。
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/private/',
},
sitemap: 'http://localhost:3000/sitemap.xml',
};
}
http://localhost:3000/robots.txt にアクセスすると robots.ts で設定した内容が表示されます。
もし robots.txt, robots.ts ファイルが app ディレクトリに存在する場合は"Duplicate page detected. app/robots.ts and app/robots.txt resolve to /robots.txt"のメッセージが npm run dev コマンドを実行しているターミナルに表示されます。
canonical
複数のページで同じコンテンツが表示される場合に検索エンジンにオリジナルコンテンツの URL を伝える必要があります。またhttp://www.localhost:3000、http://localhost:3000ののようにwwwあり、なしどちらもでもアクセス可能な場合に重複したコンテンツではないようにどちらかをcanonicalで設定します。
app ディレクトリの page.tsx ファイルで設定を行います。metaBase を設定することで canonical に"/"と設定しても自動で http://localhost:3000/となります。
export const metadata = {
metadataBase: new URL('https://localhost.com:3000'),
alternates: {
canonical: '/',
},
};
ブラウザで確認すると以下のタグが設定されます。
<link rel="canonical" href="https://localhost.com:3000/" />
metadataBase を設定していない場合の動作確認を行います。
export const metadata = {
// metadataBase: new URL('https://localhost.com:3000'),
alternates: {
canonical: '/',
},
href の値が"/"となるため metadataBase を利用していない場合は canonical にフルパスの URL を設定する必要があります。
<link rel="canonical" href="/" />
Font の設定
next/font を利用することで Font の最適化を行ってくれます。Google Font を利用することができ、デフォルトでも Google Font の Inter が設定されています。
設定は layout.tsx ファイルで行っています。
import './globals.css';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode,
}) {
return (
<html lang="en">
<body className={inter.className}>
<div>{children}</div>
</body>
</html>
);
}
別のフォントに変更したい場合にも簡単に行うことできます。フォントが変更したことがすぐにわかるように Dancing Script に変更してみましょう。
import './globals.css';
import { Dancing_Script } from 'next/font/google';
const dancingscript = Dancing_Script({ subsets: ['latin'] });
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode,
}) {
return (
<html lang="en">
<body className={dancingscript.className}>
<div>{children}</div>
</body>
</html>
);
}
ブラウザで確認するとフォントが変わっていることがわかります。
Dancing_Script フォント
フォントはページのソースを見ると下記の link タグで設定が行われています。拡張子が woff2 のフォントファイルで_next/static/media/に保存されています。ローカルへフォントファイルがダウンロードされることもわかります。
<link
rel="preload"
as="font"
href="/_next/static/media/e5f193da326e76b4-s.p.woff2"
crossorigin=""
type="font/woff2"
/>
ビルド
npm run build コマンドを実行すると本番環境用のビルドを行うことができます。ビルドを行うことによってファイルサイズや作成されるファイルだけではなく App Router で設定した各ルーティングがランタイムにサーバサイドでレンダリングするのか Static HTML としてビルド時に作成されるのか確認することができます。
% npm run build
> next-js-13@0.1.0 build
> next build
- warn Detected next.config.js, no exported configuration found. https://nextjs.org/docs/messages/empty-configuration
- info Creating an optimized production build
- info Compiled successfully
- info Linting and checking validity of types
- info Collecting page data
- info Generating static pages (7/7)
- info Finalizing page optimization
Route (app) Size First Load JS
┌ ○ / 872 B 83.3 kB
├ ○ /about 178 B 82.6 kB
├ λ /api 0 B 0 B
├ λ /api/[id] 0 B 0 B
├ ○ /favicon.ico 0 B 0 B
├ ○ /users 178 B 82.6 kB
└ λ /users/[id] 145 B 77 kB
+ First Load JS shared by all 76.8 kB
├ chunks/139-336052c7d3b6586e.js 24.4 kB
├ chunks/2443530c-1b4abb6ebb1db3b4.js 50.5 kB
├ chunks/main-app-60a08b1b1e2d662d.js 211 B
└ chunks/webpack-4449db1b67bf02a4.js 1.64 kB
Route (pages) Size First Load JS
─ ○ /404 178 B 85.9 kB
+ First Load JS shared by all 85.8 kB
├ chunks/main-8b170562f4103bba.js 83.9 kB
├ chunks/pages/_app-c544d6df833bfd4a.js 192 B
└ chunks/webpack-4449db1b67bf02a4.js 1.64 kB
λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
○ (Static) automatically rendered as static HTML (uses no initial props)
ルーティングの左に表示されている λ マークか o かによって Server Side Render なのか Static なのかがわかります。about や users などは Static ですが、Dynamic Routes を利用している/users/[id]は Sever Side Render となっています。
ビルド後.next/server/app ディレクトリを確認すると Static と表示された about や users については about.html、users.html と HTML ファイルの形として保存されています。
users.html ファイルの中身を確認するとユーザ情報を含んだ HTML であることがわかります。fetch 関数で取得したユーザ名なども確認することができます。
users.html ファイルの中身
cache オプションの設定
Static として静的ファイルを作成するかどうかは fetch 関数の cache オプションの値にも関係しており users/UserList.tsx ファイルで cache の値を'no-store'にしてビルドを実行してみます。
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
cache: 'no-store',
});
cache の値を'no-store'にする前は/users は ○ から λ に変わっていることがわかります。
% npm run build
//略
Route (app) Size First Load JS
┌ λ / 872 B 83.3 kB
├ ○ /about 178 B 82.6 kB
├ λ /api 0 B 0 B
├ λ /api/[id] 0 B 0 B
├ ○ /favicon.ico 0 B 0 B
├ λ /users 178 B 82.6 kB
└ λ /users/[id] 145 B 77 kB
.next/server/app ディレクトリを確認しても users.html は作成されていません。
generateStaticParams
/user/[id]はビルドを作成すると λ となり、静的ファイルではなくリクエストが来た時にサーバサイドレンダリングによりページが作成されます。しかし、/user/[id]の id は動的に変わりますが同じ id でアクセスがあれば同じ内容が表示されます。
generateStaticParams 関数を利用することで Dynamic Routes もビルド時に静的なファイルとして作成することができます。generateStaticParams を利用して id の値を users/[id]/page.tsx ファイルで Next.js に教えてあげる必要があります。
import type { Metadata } from 'next';
import { type User } from '../UserList';
async function getUser(id: string) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`
);
return response.json();
}
export async function generateMetadata({
params,
}: {
params: { id: string },
}): Promise<Metadata> {
const user = await getUser(params.id);
return { title: user.name };
}
export async function generateStaticParams() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users: User[] = await response.json();
return users.map((user) => ({
id: user.id.toString(),
}));
}
const Page = async ({ params }: { params: { id: string } }) => {
const user: User = await getUser(params.id);
return (
<div className="m-4">
<h1 className="text-lg font-bold">ユーザ詳細</h1>
<p>ID: {user.id}</p>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
);
};
export default Page;
generateStaticParams 関数を設定後、npm run build コマンドでビルドを行います。ビルドのメッセージを見るとこれまでの ○,λ とは異なる ● となっていることがわかります。
% npm run build
//略
Route (app) Size First Load JS
┌ λ / 872 B 83.3 kB
├ ○ /about 178 B 82.6 kB
├ λ /api 0 B 0 B
├ λ /api/[id] 0 B 0 B
├ ○ /favicon.ico 0 B 0 B
├ λ /users 178 B 82.6 kB
└ ● /users/[id] 145 B 77 kB
├ /users/1
├ /users/2
├ /users/3
└ [+7 more paths]
//略
λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
○ (Static) automatically rendered as static HTML (uses no initial props)
● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
● は SSG(Sever Site Generator)の略で静的なファイルが作成されます。
.next/server/app の users ディレクトリを確認すると 1.html, ...., 10.html まで HTML ファイルが作成されていることがわかります。このように generateStaticParams を利用することで Dyamic Routes から静的なファイルを作成することができます。
Dynamic Routes の一部の id だけ静的ファイルを作成したい場合には generateMetadata 関数で静的なファイルを作成する id のみ渡すことで実現することができます。
export async function generateStaticParams() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users: User[] = await response.json();
return users.slice(0, 3).map((user) => ({
id: user.id.toString(),
}));
}
ビルドを行うと指定した数のみ静的ファイルが作成されていることがわかります。.next/server/app の users ディレクトリを確認すると 1.html, 2.html, 3.html のファイルのみ作成されています。
├ λ /users 178 B 82.6 kB
└ ● /users/[id] 145 B 77 kB
├ /users/1
├ /users/2
└ /users/3
+ First Load JS shared by all
関数を利用せず下記のように記述することもできます。指定した/users/1, /users/4, /users/8 のみ静的ファイルが作成されます。
export function generateStaticParams() {
return [{ id: '1' }, { id: '4' }, { id: '8' }];
}
Revalidating Data
npm run build コマンドで静的ファイルを作成することができましたが現在の設定ではビルドコマンド実行時に取得したデータが更新された場合再度ビルドを行うまでページに反映されません。
アプリケーション全体を再ビルドすることなく更新した内容を反映させるために fetch 関数の next.revalidate を利用します。指定した時間が経過するとバックグランドでページの更新を行ってくれます。
設定方法は fetching の章で確認済みですが user/[id]/page.tsx ファイルで以下の設定を行います。
async function getUser(id: string) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`,
{
next: {
revalidate: 5,
},
}
);
return response.json();
}
ページの更新が本当に行われているかどうか確認するために randam メソッドを利用し乱数をブラウザ上に表示させこの値が更新されるかどうかチェックを行います。next.revalidate で設定した時間が経過して値が更新されればページの更新が行われていることがわかります。
<div className="m-4">
<h1 className="text-lg font-bold">ユーザ詳細</h1>
<p>ID: {user.id}</p>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
<p>Randam: {Math.random()}</p>
</div>
設定後は npm run build コマンドを実行します。
5 秒経過後にアクセスをするとその時のアクセスでは同じ内容が表示されます(バックグランドでデータの再取得が行われページが更新される)がその次のリクエストでは更新された乱数が表示されます。
.next/server/app/users ディレクトリに保存されている html の内容を確認していると設定した時間を経過するとファイルが再作成されていることがわかります。
このように静的ファイルも fetch 関数の next.revalidate を利用することで更新を行うことができます。
現在の引き続き更新中です。
- Alpine.js
- Astro
- chakra
- CloudFlare
- CSS
- Docker
- Drizzle
- Dropbox
- Editor
- Firebase
- Gatsuby
- GraphQL
- HTML
- JavaScript
- Jest
- Laravel
- MSW
- Next.js
- Node.js
- Nuxt
- PHP
- PlanetScale
- Playwright
- Prisma
- Python
- React
- ReactNavive
- RedwoodJS
- Remix
- Storybook
- supabase
- Svelte
- tailwindcss
- trpc
- TypeScript
- Webpack
- OS
- WordPress
- Vite
- Vue.js
- Zod
カテゴリー一覧
- Alpine.js
- Astro
- chakra
- CloudFlare
- CSS
- Docker
- Drizzle
- Dropbox
- Editor
- Firebase
- Gatsuby
- GraphQL
- HTML
- JavaScript
- Jest
- Laravel
- MSW
- Next.js
- Node.js
- Nuxt
- PHP
- PlanetScale
- Playwright
- Prisma
- Python
- React
- ReactNavive
- RedwoodJS
- Remix
- Storybook
- supabase
- Svelte
- tailwindcss
- trpc
- TypeScript
- Webpack
- OS
- WordPress
- Vite
- Vue.js
- Zod