26
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

初めに

10月後半に行われたNext.js Conf 2022で発表されたNext.js 13を実際に触ってみたのでその内容を書きます。
当初は全体を網羅して書く予定だったのですが量が多かったの少し絞ってます。
対象読者としてある程度ReactやNext.jsを触っている人を対象としています。

公式ドキュメント参考に行います
https://beta.nextjs.org/docs/getting-started

今回使用したサンプルリポジトリ
https://github.com/happy663/Next13-sample

プロジェクト作成からサーバ起動

TypeScriptとESLintを入れるか聞かれるので入れる

npx create-next-app@latest --ts

pagesディレクトリを削除

rm -rf pages

appディレクトリを作る

mkdir app

appディレクトリは実験段階の機能なので、next.config.jsを変更する

next.config.js
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  experimental: {
    appDir: true,
  },
};

app/page.tsxを作り、以下のようにする

app/page.tsx
export default function Page() {
  return <h1>Hello, Next.js!</h1>;
}

このpage.tsxがアプリケーションのルートにアクセスされた時にレンダリングされるページになります。
Next.js 13以前のpagesディレクトリのindex.tsxに相当するところですね。

ローカルサーバ起動

npm run dev

ブラウザで http://localhost:3000/ にアクセスすると、Hello, Next.js! が表示される
Image from Gyazo

LayoutとHead

サーバを起動すると自動的にappディレクトリにlayout.tsx,head.tsxというファイルが作られていました。

next/headを使って各ページファイルにheadを定義していたのがhead.tsxに書けるようになったみたいですね

head.tsx
export default function Head() {
  return (
    <>
      <title></title>
      <meta content="width=device-width, initial-scale=1" name="viewport" />
      <link rel="icon" href="/favicon.ico" />
    </>
  )
}

ページの共通レイアウトを定義するファイル

layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <head />
      <body>{children}</body>
    </html>
  )
}

今までは_app.tsxなどに下記のようにレイアウトを定義していました

_app.tsx
  <Layout>
      <Component {...pageProps} />
  </Layout>

この方法だとページごとにレイアウトを変えることができません。なのでページごとにレイアウトを変えたい場合はgetLayoutを用いる必要がありました。Next.js 13ではレイアウトを変えたいページがあるディレクトリにlayout.tsxを置くことでページごとにレイアウトを変えることができるようになりました。

ここではダッシュボードページのレイアウトの場合を考えます。
appディレクトリの中でdashbordディレクトリを作成して以下のようにlayout.tsxを配置するだけです。

app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <section>{children}</section>;
}

少し疑問に思ったのがdashboardディレクトリのpage.tsxではRootLayoutは呼ばれずDashboardLayoutのみが呼ばれるのかと思っていました。しかしそんなことはなく入れ子構造で呼び出されるようです。

公式の画像がわかりやすいので貼っておきます。
Image from Gyazo

次の機能に行く前にdashboardディレクトリにpage.tsxも作っておきます

app/dashboard/page.tsx

export default function DashBoardPage() {
  return <h1>DashBoard Page</h1>;
}

違いがわかりやすいように他のファイルも変更します

app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <head />
      <body
        style={{
          backgroundColor: '#C0C0C0',
          padding: '50px',
        }}
      >
        {children}
      </body>
    </html>
  );
}

app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <section
      style={{
        backgroundColor: 'white',            
      }}
    >
      {children}
    </section>
  );
}

ページの見た目が画像のようになってればOK
Image from Gyazo

React Server Components

React Server Components(RSC)はReact18で追加された機能でクライアントとサーバ側が協調してアプリケーションをレンダリングできる機能です。これによりコンポーネントごとに最適なレンダリング方法を選択できるようになります。例えばデータの取得はサーバ側で行い、ユーザの操作によって変わる部分はクライアント側でレンダリングするといったことができます。
これも公式ドキュメントの図がわかりやすいので貼っておきます。
Image from Gyazo

またSSRとの違いとしてクライアント側のJavaScriptの量を減らせる点です。
SSRの場合ハイドレーション(サーバ側で生成したDOMとクライアントで生成したDOMを合成する)というステップがありページを早く表示できてもクライアント側でも同じ処理が走るためJavaScriptの量は同じでした。RSCはサーバ側でレンダリングした後残りをクライアント側でレンダリングします。これによってクライアント側に送信されるJavaScriptの量を減らすことができます。

サーバーコンポーネントでデータを取得

appディレクトリ内のコンポーネントはデフォルトだとサーバコンポーネントになっています。
以下のコードはサーバサイドでqiitaの記事リスト取得して表示するものです。
dashboard/page.tsxを書き換えます。

dashboard/page.tsx
type Article = {
  id: number;
  title: string;
};

async function getArticle(): Promise<Article[]> {
  const res = await fetch('https://qiita.com/api/v2/items?page=1&per_page=24');

  if (!res.ok) {
    throw new Error('Failed to fetch data');
  }

  return res.json();
}

export default async function DashBoardPage() {
  const articles = await getArticle();

  return (
    <div>
      <h1>Dashboard</h1>
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          flexWrap: 'wrap',
          height: '50vh',
        }}
      >
        {articles?.map((article) => (
          <div
            key={article.id}
            style={{
              display: 'flex',
              gap: '10px',
            }}
          >
            <p>{article.title}</p>
          </div>
        ))}
      </div>
    </div>
  );
}


画像のように表示される
Image from Gyazo

サイトにカーソルを合わせて右クリックしてページのソースを表示をクリックしてみましょう。
あらかじめデータが入った状態でサーバから送られてくるのでページソースに記事データが表示されています。
Image from Gyazo

ローディングのUI表示

次はローディングUIを表示する機能です
dashboradディレクトリにloading.tsxを作成します

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

データの取得が終わりpageコンポーネントがレンダリングされるまでの間はLoadingコンポーネントが表示されます

Image from Gyazo

これはReact18で追加されたSuspenseという機能が使われていてます。
Suspenseについて説明するとコンポーネントが表示されるまでの状態を指定することができるコンポーネントです。非同期的なコンポーネントの場合レンダリングに時間がかかるためその間に何を表示させるかをSuspenseを使うと指定できるようになります。

具体的な使用例を上げます。下のコードはデータフェッチライブラリ React Queryを使ったデータ取得と表示のサンプルです。


import { QueryClient, QueryClientProvider, useQuery } from 'react-query';

const queryClient = new QueryClient();

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  );
}
export function Loading() {
  return <p>Loading...</p>;
}

function Example() {
  const { isLoading, data } = useQuery('repoData', () =>
    fetch('https://api.github.com/repos/tannerlinsley/react-query').then(
      (res) => res.json()
    )
  );

  if (isLoading) return <Loading />;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
    </div>
  );
}

Suspenseを使えば下のコードに置き換えることができます

import { Suspense } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';

const queryClient = new QueryClient();

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* suspenseコンポーネントででラップ */}
      <Suspense fallback={<Loading />}>
        <Example />
      </Suspense>
    </QueryClientProvider>
  );
}
export function Loading() {
  return <p>Loading...</p>;
}

function Example() {
  const { data } = useQuery('repoData', () =>
    fetch('https://api.github.com/repos/tannerlinsley/react-query').then(
      (res) => res.json()
    )
  );
  //ローディングプロパティによる表示分岐の削除

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
    </div>
  );
}

変更点はExampleコンポーネントをSuspenseコンポーネントでラップしているのと、Exampleコンポーネントの中のisLoadingプロパティを使った表示切替の部分が消えているところです。Suspenseのいいところはデータ取得をするコンポーネントの中で表示を切り替える処理を書く必要がないことです。これによってより宣言的なコードになりました。またコンポーネントの責務の観点から見てもローディング完了時の表示だけでよくシンプルになっています。

Next.js 13ではloading.tsxを置いてあげるとNext.js側がそれを読み取りPageコンポーネントをSuspenseコンポーネントでラップしてくれるようです。
普通に書くと以下のようになります。

<Layout>
  <Headr/>
  <SideNav/>
  <Suspense fallback={<Loading />}>
    <DashBoardPage />
  </Suspense>
</Layout>

ディレクトリ内にloading.tsxを置いておけば自動的にこのコードを書いた動きになるということですね。

エラーハンドリング

次にエラーハンドリングを見ていきます。
dashboardディレクトリにerror.tsxを作成します。

dashboard/error.tsx
'use client';

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error);
  }, [error]);

  return (
    <div>
      <p>Something went wrong!</p>
      <button onClick={() => reset()}>Reset error boundary</button>
    </div>
  );
}

公式ドキュメントによるとerror.tsxはクライアントコンポーネントである必要がありuse client;と書くことでクライアントコンポーネントして利用することができるようです。
loading.tsxと同じようにファイルを置いておけば自動的にPageをネストしてエラーハンドリングをしているようです。

普通に書いた場合

<Layout>
  <Headr/>
  <SideNav/>
  <ErrorBoundary fallback={<Loading />}>
    <DashBoardPage />
  </ErrorBoundary>
</Layout>

ただこれを見てわかる通りpageをラップしてるので同階層のコンポーネントのエラーハンドリングはできない感じですね。したいならより上の階層でErrorBoundaryを使う必要がありそうです。

実際にコードを書き換えてエラーを出してみます。
存在しないURL'hoge'を指定しgetArticelでしていたエラーハンドリングを削除しています。

dashboard/page.tsx

type Article = {
  id: number;
  title: string;
};

async function getArticle(): Promise<Article[]> {
  const res = await fetch('hoge');

  return res.json();
}

export default async function DashBoardPage() {
  const articles = await getArticle();

  return (
    <div>
      <h1>Dashboard</h1>
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          flexWrap: 'wrap',
          height: '50vh',
        }}
      >
        {articles?.map((article) => (
          <div
            key={article.id}
            style={{
              display: 'flex',
              gap: '10px',
            }}
          >
            <p>{article.title}</p>
          </div>
        ))}
      </div>
    </div>
  );
}



エラーの内容とエラーページが表示されていますね

Image from Gyazo

終わりに

感想としてはNext.js 13の機能はReact18のSupenseやRSCなどの機能に合わせたアップデートというのを強く感じました

26
5
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
26
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?