Next.jsの勉強がてら公式のチュートリアルを1からなぞってみました。
実際にチュートリアルをベースに書いたソースコードはこちら
- Next.js基礎コース App Router やってみた 1 ~ 3
- Next.js基礎コース App Router やってみた 4 ~ 6
- Next.js基礎コース App Router やってみた 7 ~ 9
- Next.js基礎コース App Router やってみた 10 ~ 12
- Next.js基礎コース App Router やってみた 13 ~ 15
- Next.js基礎コース App Router やってみた 16 ~ 17
4. レイアウトとページの作成
レイアウト と ページ を使ってより多くのルートを作成する方法を学びましょう
Next.jsはファイルシステムルーティングを採用しており、フォルダを用いてネストされたルートを作成します。
各ルートごとに、layout.tsx と page.tsx ファイルを使用して個別の UI を作成できます。
page.tsx は React コンポーネントをエクスポートする特別な Next.js ファイルであり、ルートにアクセス可能にするために必須です。
※ つまり page.txt が配置してあるディレクトリが公開可能なルートとなります
ダッシュボードページの作成
/app/dashboard/page.tsx
export default function Page() {
return <p>Dashboard Page</p>;
}
/app/dashboard/customers/page.tsx
export default function Page() {
return <p>Customers Page</p>;
}
/app/dashboard/invoices/page.tsx
export default function Page() {
return <p>Invoices Page</p>;
}
ダッシュボードレイアウトの作成
ダッシュボードには、複数のページで共有されるナビゲーション機能があります。Next.jsでは、特別なlayout.tsxファイルを使って複数のページで共有されるUIを作成できます。
/app/dashboard/layout.tsx
import SideNav from '@/app/ui/dashboard/sidenav'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
<div className="w-full flex-none md:w-64">
<SideNav />
</div>
<div className="flex-grow p-6 md:overflow-y-auto md:p-12">{children}</div>
</div>
);
}
このレイアウトはそれ以下のページ(dashboard/customers/page.tsx dashboard/invoices/page.tsx)に自動的に適用されます。
Next.jsでレイアウトを使用する利点の一つは、ナビゲーション時にページコンポーネントのみが更新され、レイアウトは再レンダリングされないことです。(部分レンダリング)
ルートレイアウト
/app/layout.tsx はルートレイアウトと呼ばれ、すべてのNext.jsアプリケーションに必須です。ルートレイアウトに追加したUIは、すべてのページで共有されます。
/app/layout.tsx
import '@/app/ui/global.css';
import { inter } from '@/app/ui/fonts';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
{/* antialiased Tailwindのフォントを滑らかにするクラス */}
<body className={`${inter.className} antialiased`}>{children}</body>
</html>
);
}
5. ページ間の移動
<Link> コンポーネント
Next.js では、コンポーネントを使用してアプリケーション内のページ間をリンクできます。
/app/ui/dashboard/nav-links.tsx
import {
UserGroupIcon,
HomeIcon,
DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link'; // 追加
// Map of links to display in the side navigation.
// Depending on the size of the application, this would be stored in a database.
const links = [
{ name: 'Home', href: '/dashboard', icon: HomeIcon },
{
name: 'Invoices',
href: '/dashboard/invoices',
icon: DocumentDuplicateIcon,
},
{ name: 'Customers', href: '/dashboard/customers', icon: UserGroupIcon },
];
export default function NavLinks() {
return (
<>
{links.map((link) => {
const LinkIcon = link.icon;
return (
{/* Linkコンポーネントでページ遷移する */}
<Link
key={link.name}
href={link.href}
className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"
>
<LinkIcon className="w-6" />
<p className="hidden md:block">{link.name}</p>
</Link>
);
})}
</>
);
}
自動コード分割とプリフェッチ
React SPA では初回アクセス時にアプリケーションのすべてのコードをブラウザがロードしていましたが、Next.jsは自動的にルートセグメントごとにアプリケーションコードを分割してロードします。
コード分割により、ブラウザが解析するコードが減るのでアプリケーションが高速化するとともに、特定のページでエラーが発生していてもアプリケーション全体が動作しなくなる事がなくなります。
本番環境では <Link> コンポーネントがブラウザの表示領域に表示されるたびに、Next.jsはバックグラウンドでリンク先ルートのコードを自動的にプリフェッチします。
アクティブなリンクを表示する
現在どのページにいるかを示すために、自分が開いているページのナビゲーションリンクの色を変えます。
これを行うにはユーザーの現在のパスを取得する必要があります。これには usePathname() が利用できます。
※ usePathname はReactフックなので、クライアントコンポーネントで呼び出す必要があります。
/app/ui/dashboard/nav-links.tsx
'use client';
// ...
import { usePathname } from 'next/navigation'; // 追加
// ...
export default function NavLinks() {
return (
<>
{links.map((link) => {
const pathname = usePathname(); // 現在のパスを取得
const LinkIcon = link.icon;
return (
<Link
key={link.name}
href={link.href}
// 現在のパスと同じ場合はカラーを変更
className={clsx(
"flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3",
{
'bg-sky-100 text-blue-600': pathname === link.href,
}
)}
>
<LinkIcon className="w-6" />
<p className="hidden md:block">{link.name}</p>
</Link>
);
})}
</>
);
}
6. データベースの設定
Postgresqlの起動
./bin/postgresql.sh -h
ログイン
PGPASSWORD=root1234 psql -U app -h sample-postgresql -d sample -p 5432
基本操作
# DB一覧
\l
# use database
\c <DB_NAME>
# テーブル一覧
\dt
# テーブル一覧(viewやsequenceも含む)
\d
# テーブルのスキーマ確認
\d <TABLE_NAME>
# テーブルのアクセス権限確認
\z <TABLE_NAME>
# ユーザー一覧を表示
\du
環境変数ファイルの作成
cp .env.example .env
.env
# Copy from .env.local on the Vercel dashboard
# https://nextjs.org/learn/dashboard-app/setting-up-your-database#create-a-postgres-database
POSTGRES_URL=postgresql://app:root1234@sample-postgresql:5432/sample
POSTGRES_PRISMA_URL=
POSTGRES_URL_NON_POOLING=
POSTGRES_USER=app
POSTGRES_HOST=sample-postgresql
POSTGRES_PASSWORD=root1234
POSTGRES_DATABASE=sample
# `openssl rand -base64 32`
AUTH_SECRET=
AUTH_URL=http://localhost:3000/api/auth
DBのシーディング
シーディング用のルートを少し修正
app/seed/route.ts
// ...
// ローカルのDBに接続するので ssl=false に
// connection設定: https://github.com/porsager/postgres?tab=readme-ov-file#connection-details
//const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
const sql = postgres(process.env.POSTGRES_URL!, {ssl: false});
async function seedUsers() {
// 複数回実行するとエラーになるのでコメントアウト
// await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
// ...
return insertedUsers;
}
async function seedInvoices() {
// 複数回実行するとエラーになるのでコメントアウト
// await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
// ...
return insertedInvoices;
}
async function seedCustomers() {
// 複数回実行するとエラーになるのでコメントアウト
// await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
// ...
return insertedCustomers;
}
async function seedRevenue() {
// ...
}
export async function GET() {
try {
// CREATE EXTENSIONはここに一回だけ定義
await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
const result = await sql.begin((sql) => [
seedUsers(),
seedCustomers(),
seedInvoices(),
seedRevenue(),
]);
return Response.json({ message: 'Database seeded successfully' });
} catch (error) {
return Response.json({ error }, { status: 500 });
}
}
シーディングURLにアクセス
確認
PGPASSWORD=root1234 psql -U app -h sample-postgresql -d sample -p 5432
# psql (16.10 (Ubuntu 16.10-0ubuntu0.24.04.1))
# Type "help" for help.
sample=#\d
# List of relations
# Schema | Name | Type | Owner
#--------+-----------+-------+-------
# public | customers | table | app
# public | invoices | table | app
# public | revenue | table | app
# public | users | table | app
#(4 rows)
sample=# \d customers;
# Table "public.customers"
# Column | Type | Collation | Nullable | Default
#-----------+------------------------+-----------+----------+--------------------
# id | uuid | | not null | uuid_generate_v4()
# name | character varying(255) | | not null |
# email | character varying(255) | | not null |
# image_url | character varying(255) | | not null |
#Indexes:
# "customers_pkey" PRIMARY KEY, btree (id)
sample=# \d invoices;
# Table "public.invoices"
# Column | Type | Collation | Nullable | Default
#-------------+------------------------+-----------+----------+--------------------
# id | uuid | | not null | uuid_generate_v4()
# customer_id | uuid | | not null |
# amount | integer | | not null |
# status | character varying(255) | | not null |
# date | date | | not null |
#Indexes:
# "invoices_pkey" PRIMARY KEY, btree (id)
sample=# \d revenue;
# Table "public.revenue"
# Column | Type | Collation | Nullable | Default
#---------+----------------------+-----------+----------+---------
# month | character varying(4) | | not null |
# revenue | integer | | not null |
#Indexes:
# "revenue_month_key" UNIQUE CONSTRAINT, btree (month)
sample=# \d users;
# Table "public.users"
# Column | Type | Collation | Nullable | Default
#----------+------------------------+-----------+----------+--------------------
# id | uuid | | not null | uuid_generate_v4()
# name | character varying(255) | | not null |
# email | text | | not null |
# password | text | | not null |
#Indexes:
# "users_pkey" PRIMARY KEY, btree (id)
# "users_email_key" UNIQUE CONSTRAINT, btree (email)
sample=# SELECT invoices.amount, customers.name
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
WHERE invoices.amount = 666;
# amount | name
# --------+-------------
# 666 | Evil Rabbit
# 666 | Evil Rabbit
# (2 rows)