Next.js 13.4 で React Server Component を試す
このスクラップについて
2023 年 5 月5 日(金)に Next.js 13.4 がリリースされた。
Next.js 13.4 では App Router が stable になり、今後本格的に普及していくと思われる。
このスクラップでは実際に App Router を使って色々試してみてその過程を記録していきたい。
create-next-app
当り前だけど create-next-app も13.4.1 がリリースされている。
create-next-app バージョン確認
npx create-next-app --version
13.4.1
create-next-app ヘルプ確認
npx create-next-app --help
Usage: create-next-app <project-directory> [options]
Options:
-V, --version output the version number
--ts, --typescript
Initialize as a TypeScript project. (default)
--js, --javascript
Initialize as a JavaScript project.
--tailwind
Initialize with Tailwind CSS config. (default)
--eslint
Initialize with eslint config.
--app
Initialize as an App Router project.
--src-dir
Initialize inside a `src/` directory.
--import-alias <alias-to-configure>
Specify import alias to use (default "@/*").
--use-npm
Explicitly tell the CLI to bootstrap the application using npm
--use-pnpm
Explicitly tell the CLI to bootstrap the application using pnpm
-e, --example [name]|[github-url]
An example to bootstrap the app with. You can use an example name
from the official Next.js repo or a GitHub URL. The URL can use
any branch and/or subdirectory
--example-path <path-to-example>
In a rare case, your GitHub URL might contain a branch name with
a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar).
In this case, you must specify the path to the example separately:
--example-path foo/bar
--reset-preferences
Explicitly tell the CLI to reset any stored preferences
-h, --help output usage information
--app
オプションから前まであった experimental が無くなっている。
プロジェクト作成
npx create-next-app hello-app-router \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*" \
--use-npm
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}>{children}</body>
</html>
)
}
http://localhost:3000/ の HTML コード
export const metadata
を使うと title タグや meta タグが良い感じに出力されるようだ。
page.tsx を書き換えてみる
タイマーを使ってサーバー側でのデータ取得を模倣してみる。
type Topic = {
id: number;
title: string;
publishedAt: Date;
};
export default async function Home() {
const topics = await new Promise<Topic[]>((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, title: "Topic title 1", publishedAt: new Date() },
{ id: 2, title: "Topic title 2", publishedAt: new Date() },
{ id: 3, title: "Topic title 3", publishedAt: new Date() },
]);
}, 500);
});
return (
<main className="container mx-auto px-4">
<h1 className="text-4xl mt-3 mb-3">Home</h1>
<section>
<h2 className="text-2xl mb-3">Topics</h2>
<ul>
{topics.map((topic) => (
<li key={topic.id}>
{topic.title} / {topic.publishedAt.toDateString()}
</li>
))}
</ul>
</section>
</main>
);
}
地味に嬉しいこと
Date オブジェクトをそのまま渡せる。
getServerSideProps だと文字列に変換するか superjson を使う必要があるのでこれは便利だ。
データを取得するコードと HTML を近くに書けるのも複数ファイルを切り替える必要がなくて良い。
まあこの点については getServerSideProps を使った場合と同じだが。
useState() を使おうとしてみる
import { useState } from "react";
type Topic = {
id: number;
title: string;
publishedAt: Date;
};
export default async function Home() {
const [counter, setCounter] = useState(0);
const topics = await new Promise<Topic[]>((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, title: "Topic title 1", publishedAt: new Date() },
{ id: 2, title: "Topic title 2", publishedAt: new Date() },
{ id: 3, title: "Topic title 3", publishedAt: new Date() },
]);
}, 500);
});
return (
<main className="container mx-auto px-4">
<h1 className="text-4xl mt-3 mb-3">Home</h1>
<section>
<h2 className="text-2xl mb-3">Topics</h2>
<ul>
{topics.map((topic) => (
<li key={topic.id}>
{topic.title} / {topic.publishedAt.toDateString()}
</li>
))}
</ul>
</section>
</main>
);
}
期待通りエラーが発生した。
ここで Client Component の出番
touch src/app/counter.tsx
"use client";
import { useState } from "react";
export default function Counter() {
const [counter, setCounter] = useState(0);
return (
<>
<button
className="border p-1 mb-3"
type="button"
onClick={() => setCounter(counter + 1)}
>
Increment
</button>
<dl>
<dt>Counter</dt>
<dd>{counter}</dd>
</dl>
</>
);
}
import Counter from "./counter";
type Topic = {
id: number;
title: string;
publishedAt: Date;
};
export default async function Home() {
const topics = await new Promise<Topic[]>((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, title: "Topic title 1", publishedAt: new Date() },
{ id: 2, title: "Topic title 2", publishedAt: new Date() },
{ id: 3, title: "Topic title 3", publishedAt: new Date() },
]);
}, 500);
});
return (
<main className="container mx-auto px-4">
<h1 className="text-4xl mt-3 mb-3">Home</h1>
<section className="mb-3">
<h2 className="text-2xl mb-3">Topics</h2>
<ul>
{topics.map((topic) => (
<li key={topic.id}>
{topic.title} / {topic.publishedAt.toDateString()}
</li>
))}
</ul>
</section>
<section className="mb-3">
<h2 className="text-2xl mb-3">Countere</h2>
<Counter></Counter>
</section>
</main>
);
}
実行結果
Tailwind CSS IntelliSense
VSCode にインストールした。
Layout コーディング
import Link from "next/link";
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}>
<header>
<nav aria-label="Header" className="p-4 border-b">
<Link href="/">Hello App Router</Link>
</nav>
</header>
{children}
</body>
</html>
);
}
実行結果
Layout とは関係ないが Inter って何だろう?
Next.js Fonts ドキュメント
Inter は字体のようだ
Noto や Roboto などと同じ。
可変フォント
可変フォント (Variable fonts) は幅、太さ、スタイルごとに個別のフォントファイルを用意するのではなく、書体のさまざまなバリエーションを 1 つのファイルに組み込むことができる OpenType フォント仕様の進化版です。
可変フォントの場合は weight を指定する必要がある。
import { Roboto } from 'next/font/google';
const roboto = Roboto({
weight: '400',
subsets: ['latin'],
});
Google Fonts の可変フォントは下記から調べられる。
下層ページを作る
mkdir src/app/about
touch src/app/about/page.tsx
import Link from "next/link";
export default function About() {
return (
<main className="container mx-auto p-4">
<h1 className="mb-3">About</h1>
<Link href="/" className="border p-2 inline-block">
← Home
</Link>
</main>
);
}
下層ページにも src/app/layout.tsx がしっかり適用されている。
トップページにリンクを設ける
import Link from "next/link";
import Counter from "./counter";
type Topic = {
id: number;
title: string;
publishedAt: Date;
};
export default async function Home() {
const topics = await new Promise<Topic[]>((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, title: "Topic title 1", publishedAt: new Date() },
{ id: 2, title: "Topic title 2", publishedAt: new Date() },
{ id: 3, title: "Topic title 3", publishedAt: new Date() },
]);
}, 500);
});
return (
<main className="container mx-auto px-4">
<h1 className="text-4xl mt-3 mb-3">Home</h1>
<section className="mb-3">
<h2 className="text-2xl mb-3">Topics</h2>
<ul>
{topics.map((topic) => (
<li key={topic.id}>
{topic.title} / {topic.publishedAt.toDateString()}
</li>
))}
</ul>
</section>
<section className="mb-3">
<h2 className="text-2xl mb-3">Countere</h2>
<Counter></Counter>
</section>
<section className="mb-3">
<h2 className="text-2xl mb-3">Navigation</h2>
<Link href="/about" className="border p-2 inline-block">
About →
</Link>
</section>
</main>
);
}
Typo
Counter が Countere になっていた。