🎉

Next.js 13 で開発方法はどう変わる?

2022/10/27に公開約8,900字4件のコメント

Next.js 13 が発表されましたね!

https://nextjs.org/blog/next-13

この記事では Next.js の開発方法が大きく変わるポイントとなる以下の3つの新機能について取り上げます。

  • Layouts
  • React Server Components
  • Streaming

それではさっそく試していくことにしましょう!

なおサンプルのソースコードについては以下で公開しています。

https://github.com/andraindrops/nextjs13-sample

インストール

ウェブアプリの雛形は以下で作成できます。

npx create-next-app nextjs13-sample --ts --use-npm

next.config.jsappDirを有効にすることで今回紹介する新機能を使えるようになります。

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

参考:
https://dev.classmethod.jp/articles/next-js-13-sample/

サンプルの作成

ページを作成します。
*ルートディレクトリは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 の目玉機能で、
複数のページ間でレイアウトおよび内部状態を共有することができます。

https://beta.nextjs.org/docs/routing/pages-and-layouts

レイアウトをつくって挙動を確認してみましょう。

自動生成されたapp/layout.tsxを以下のように上書きします。

_app.tsxおよび_document.tsxlayout.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.tsxapp/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
を使うことが推奨されています。

https://beta.nextjs.org/docs/rendering/server-and-client-components#when-to-use-server-vs-client-components

また以下の点に注意しましょう。

サンプルの作成

では実際に 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を開いてみます。

表示されました。

HTML ソースコードを確認するとサーバーサイドレンダリングされていることが確認できます。

サーバーサイドのコンポーネントについて、
誤ってクライアントサイドで利用する事態を避けるためserver-onlyの設定が推奨されています。

https://beta.nextjs.org/docs/rendering/server-and-client-components#keeping-server-only-code-out-of-client-components-poisoning

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を開いてみます。

作成されました。

作成後のデータのリフッシュ方法などは以下を確認ください。

https://beta.nextjs.org/docs/data-fetching/mutating

Streaming

データの読み込みのローディング表示もサポートされました。

https://beta.nextjs.org/docs/data-fetching/streaming-and-suspense

ローディングを作成します。

// app/todos/loading.tsx

export default function Loading() {
  return <p>Loading...</p>;
}

対象ページ/todosを開いてみます。

表示されました。

また読み込み完了後に一覧が表示されます。

おわりに

以上、簡単にですがリリースされた Next.js 13 の機能について説明・実装していきました。
なにか不明点や疑問点などありましたら、こちらのコメント欄か私のTwitterまでお気軽にどうぞ!

Discussion

*_app.tsxおよび_document.tsxlayout.tsxに統一されました

_app.tsxlayout.tsx_document.tsxhead.tsxになってた気がします(間違ってたらゴメンナサイ)

有用な記事ありがとうございます!
create-next-app について、一つ見つけました。

 18:38:38  > npx create-next-app --help 
~~~ 省略 ~~~
Options:
  -V, --version                      output the version number
  --ts, --typescript                 
    Initialize as a TypeScript project.
   --experimental-app
    Initialize as a `app/` directory project.

下記のようにすれば、appDirの云々が省略できそうですね。
appDir: true, になっていることは確認できました。

npx create-next-app nextjs13-sample --ts --experimental-app --use-npm

こちらこそ読んでいただいてありがとうございます!
おおー、--experimental-appってオプションがあるんですね!
本文に追記させていただきました🙏

ログインするとコメントできます