0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js基礎コース App Router やってみた 4 ~ 6

Last updated at Posted at 2026-01-31

Next.jsの勉強がてら公式のチュートリアルを1からなぞってみました。

実際にチュートリアルをベースに書いたソースコードはこちら

4. レイアウトとページの作成

レイアウトページ を使ってより多くのルートを作成する方法を学びましょう

Next.jsはファイルシステムルーティングを採用しており、フォルダを用いてネストされたルートを作成します。

04_nested_routing.png (94.6 kB)

各ルートごとに、layout.tsxpage.tsx ファイルを使用して個別の UI を作成できます。
page.tsx は React コンポーネントをエクスポートする特別な Next.js ファイルであり、ルートにアクセス可能にするために必須です。

※ つまり page.txt が配置してあるディレクトリが公開可能なルートとなります

04_nested_routing_02.png (72.1 kB)

ダッシュボードページの作成

/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)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?