Next.js 13で開発方法はどう変わる?
Next.js 13が発表されましたね!
この記事ではNext.jsの開発方法が大きく変わるポイントとなる以下の3つの新機能について取り上げます。
- Layouts
- React Server Components
- Streaming
それではさっそく試していくことにしましょう!
インストール
ウェブアプリの雛形は以下で作成できます。
npx create-next-app nextjs13-sample --ts --use-npm
next.config.js
のappDir
を有効にすることで今回紹介する新機能を使えるようになります。
experimental: {
appDir: true,
},
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
experimental: {
appDir: true,
},
}
module.exports = nextConfig
2022-10-31 追記:
オプション--experimental-app
をつけることで設定ファイルの変更も行われるようです。
@takashiaihara さん教えていただいてありがとうございます!
npx create-next-app nextjs13-sample --ts --experimental-app --use-npm
参考:
サンプルの作成
ページを作成します。
*ルートディレクトリはapp
に変更されました
*index ファイルはindex.tsx
からpage.tsx
に変更されています
// app/page.tsx
export default function Page() {
return <h1>Hello, Next.js!</h1>;
}
URLの重複を避けるためpages
以下は削除します。
*pages
以下のディレクトリもNext 13で引き続き利用することは可能ですがapp
の機能は利用することができません
rm -rf pages
サーバーを起動します。
npm run dev
ページを表示します。
http://localhost:3000
`Hello, Next.js! と表示されれば成功です。
Layouts
Layouts機能はNext.js 13の目玉機能で、
複数のページ間でレイアウトおよび内部状態を共有することができます。
レイアウトをつくって挙動を確認してみましょう。
自動生成されたapp/layout.tsx
を以下のように上書きします。
_app.tsx
および_document.tsx
はlayout.tsx
に統一されました。
2022-10-29 追記:
移行方法は以下を確認ください。
Migrating _document.js and _app.js
個別に Header 情報を変更する場合はhead.tsx
を利用することができます。
Modifying <head>
// app/lauout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<head></head>
<body>
<div>layout header</div>
<div>{children}</div>
</body>
</html>
);
}
トップページ/
を開いてみましょう。
以下のようにlayout header
と表示されレイアウトが適用されたことがわかります。
これは他のページにも同様に適用されます。
app/dir1/page.tsx
app/dir2/page.tsx
をそれぞれつくって表示してみましょう。
// app/dir1/page.tsx
export default function Page() {
return <h1>Hello, dir 1</h1>;
}
// app/dir2/page.tsx
export default function Page() {
return <h1>Hello, dir 2</h1>;
}
対象ページ/dir1
,/dir2
を開いてみましょう。
レイアウトが適用されていますね。
なおapp/dir1/layout.tsx
やapp/dir2/layout.tsx
を作成した場合は、
レイアウトが入れ子になって表示されます。
// app/dir1/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section>
<div>nested layout</div>
<div>{children}</div>
</section>
);
}
対象ページ/dir1
を開いてみます。
状態保存
レイアウト内部の状態は保存されます。
確認してみましょう。
レイアウトにカウンターを確認用に設置します。
"use client";
import { useState } from "react";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const [count, setCount] = useState(0);
return (
<html>
<head></head>
<body>
<div>layout header</div>
<div>{count}</div>
<button
onClick={() => {
setCount(count + 1);
}}
>
+
</button>
<div>{children}</div>
</body>
</html>
);
}
app/dir1/page.tsx
およびapp/dir2/page.tsx
に、
それぞれへのリンクを設置します。
// app/dir1/page.tsx
import Link from "next/link";
export default function Page() {
return (
<>
<h1>Hello, dir 1</h1>
<Link href="/dir2">dir 2</Link>
</>
);
}
// app/dir2/page.tsx
import Link from "next/link";
export default function Page() {
return (
<>
<h1>Hello, dir 2</h1>
<Link href="/dir1">dir 1</Link>
</>
);
}
ここから対象ページdir1
もしくはdir2
開いて、
カウントを+
してdir2
に遷移、
または、
カウントを+
してdir1
に遷移、
しても、
カウントがリセットされず、
状態が保存されていることがわかります。
React Server Components
React Sever ComponentsがNext.jsで利用できるようになりました。
実際にウェブアプリたとえば ToDo アプリをつくる場合、
表示系(一覧や詳細)はServer Components
操作系(作成や削除)はClient Components
を使うことが推奨されています。
また以下の点に注意しましょう。
- コンポーネントのデフォルトのレンダリング方法はサーバーサイドです
クライアントサイドのレンダリングの場合"use client"
と明示する必要があります
https://beta.nextjs.org/docs/rendering/server-and-client-components#convention -
SSR/ISRは
fetch
リクエストによる方法に統一されます
https://beta.nextjs.org/docs/rendering/static-and-dynamic-rendering
サンプルの作成
では実際にToDoアプリを作成してみましょう。
API
前準備としてモックAPIを用意します。
// pages/api/todos/index.ts
import type { NextApiRequest, NextApiResponse } from "next";
type Todo = {
title: string;
};
export default async (req: NextApiRequest, res: NextApiResponse<Todo[]>) => {
await new Promise((resolve) => setTimeout(resolve, 3000)); // for slow test
res
.status(200)
.json([{ title: "task 1" }, { title: "task 2" }, { title: "task 3" }]);
};
ToDoの表示
Server Componentsを利用してAPIを呼び出してみましょう。
タスクの一覧のソースコードです。
// app/todos/page.tsx
import { use } from "react";
async function getData() {
const res = await fetch("http://localhost:3000/api/todos");
// The return value is *not* serialized
// You can return Date, Map, Set, etc.
return res.json();
}
type Todo = {
title: string;
};
export default function Page() {
const todos: Todo[] = use(getData());
return (
<>
<h1>Todos</h1>
{todos.map((todo) => {
return <div>{todo.title}</div>;
})}
</>
);
}
対象ページ/todos
を開いてみます。
表示されました。
ソースコードを確認するとサーバーサイドレンダリングされていることが確認できます。
サーバーサイドのコンポーネントについて、
誤ってクライアントサイドで利用する事態を避けるためserver-only
の設定が推奨されています。
ToDoの作成
タスクの作成のソースコードです。
*擬似的なものになります
// app/todos/new/page.tsx
import { useState } from "react";
export default function Page() {
const [value, setValue] = useState("");
return (
<>
<h1>New Todo</h1>
<form
onSubmit={() => {
alert("Submitted!");
}}
>
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
<input type="submit" />
</form>
</>
);
}
対象ページ/todos/new
を開いてみます。
しかしながらこのコードは以下のようにエラーが表示されてしまいます。
というのもサーバーサイドコンポーネントであるにも関わらずuseState
を設定しているためですね(やさしい Next.js ……)。
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.
"use client"
を明示することでクライアントサイドのレンダリングであることを伝えましょう。
// app/todos/new/page.tsx
"use client";
import { useState } from "react";
export default function Page() {
const [value, setValue] = useState("");
return (
<>
<h1>New Todo</h1>
<form
onSubmit={() => {
alert("Submitted!");
}}
>
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
<input type="submit" />
</form>
</>
);
}
対象ページ/todos/new
を開いてみます。
作成されました。
作成後のデータのリフッシュ方法などは以下を確認ください。
Streaming
データの読み込みのローディング表示もサポートされました。
ローディングを作成します。
// app/todos/loading.tsx
export default function Loading() {
return <p>Loading...</p>;
}
対象ページ/todos
を開いてみます。
表示されました。
また読み込み完了後に一覧が表示されます。
おわりに
以上、簡単にですがリリースされたNext.js 13の機能について説明・実装していきました。
なにか不明点や疑問点などありましたら、こちらのコメント欄か私のTwitterまでお気軽にどうぞ!
Discussion
_app.tsx
はlayout.tsx
、_document.tsx
はhead.tsx
になってた気がします(間違ってたらゴメンナサイ)コメントありがとうございます!
根拠は以下のドキュメントになっています。
head.tsx
で Header の個別設定もできるようですね!(便利)本文中に追記させていただきました。
有用な記事ありがとうございます!
create-next-app について、一つ見つけました。
下記のようにすれば、appDirの云々が省略できそうですね。
appDir: true, になっていることは確認できました。
こちらこそ読んでいただいてありがとうございます!
おおー、
--experimental-app
ってオプションがあるんですね!本文に追記させていただきました🙏