Next.jsの特殊なファイルをサクッと紹介

tanesan

こんにちは。最近クラフトビールにはまっているtanesanです。
さっそくですがみなさんNext.jsを利用していますでしょうか?Next.jsはファイルベースのルーティングが非常に便利で初めて利用したときは感動した記憶があります。
今回はそんなNext.jsの特殊な役割を持つファイルについて勉強しましたので簡単に紹介していこうと思います!

特殊なファイル

Next.jsにはいくつか特別な役割を持つファイルが存在します。
今回ご紹介する特殊な役割を持つファイルは以下の通りです。

  • layout.js
  • route.js
  • loading.js
  • error.js
  • global-error.js
  • template.js
  • not-found.js

結構ありますよね。今回ピックアップしなかったのも含めると10以上はある気がします。
さっそくそれぞれの役割を確認していきましょう!
※ここではすべてlocalhost:3000でNext.jsアプリケーションを起動している前提で説明をします。
※最終的なコードはこちらにあります。説明の都合上、中間のコードが出てきますが、リポジトリには存在しない点に注意してください。

page.js 

特殊な役割を持つファイルのなかで最も基本的なものはpage.jsでしょうか。page.jsはページに対応するUIを定義するファイルで、appディレクトリ配下に配置することで、appディレクトリをルートとしたpage.jsへのパスをURLパスとした画面を構成することができます。

例えば /app/tanesan/page.jsを下記のように作成します。

// /app/tanesan/page.js

export default function Page() {
  return <div>tanesan</div>;
}  

次に/tanesanにアクセスすると、page.jsの内容を確認することができます。このようにpage.jsをapp配下の任意のディレクリに配置することで簡単にアプリケーションのページ階層を表現することができます。

layout.js

layout.jsはpage.jsの次に重要なファイルです。それぞれのページで共通して表示するコンポーネントを指定する時に使用するファイルがlayout.jsです。layout.jsをディレクトリに配置することで、そのディレクト配下のすべてのページにlayout.jsで記述した内容が適用することができます。


例として/app/layout.jsを作成します。

// /app/layout.js

import NavBar from '@/components/NavBar'
import Footer from '@/components/Footer'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <NavBar />
        <main>{children}</main>
        <Footer />
      </body>
    </html>
  )
}

上記で使用している、NavBarとFooterは下記の通りです。

// /components/NavBar.js

export default function NavBar() {
    return (
        <nav>
            <ul>
                <li>
                    <a href="/">Home</a>
                </li>
                <li>
                    <a href="/tanesan">Tanesan</a>
                </li>
            </ul>
        </nav>
    );
}  

// /components/Footer.js

export default function Footer() {
    return (
        <footer>
            <p>© 2024 Tanesan</p>
        </footer>
    );
}  

appディレクトリ直下のlayout.jsに、NavBarとFooterを追加することで、すべてのページにナビゲーションメニューとフッターを表示することができます。このとき、page.jsの内容は、layout.jsのchildrenに引き渡され表示されます。試しに/tanesanにアクセスしてみると、ナビゲーションとフッターが追加されていることが確認できると思います。また、appディレクトリ直下のlayout.jsはルートレイアウトと言います。このルートレイアウトには、htmlタグとbodyタグでchildrenを囲む必要があります。

route.js

route.jsはAPIの定義に使用されるファイルです。route.jsにハンドラーを定義することで、
ディレクトリ階層に対応したエンドポイントのAPIを簡単に作成できます。
例として、app/api/items配下にroute.jsを定義します。

// /app/api/items/route.js

export function GET() {
    const items = [
        { id: 1, name: 'item 1' },
        { id: 2, name: 'item 2' },
    ]
   
    return Response.json({ items })
}    

ブラウザから/api/itemsにアクセスしてみましょう。
{"items":[{"id":1,"name":"item 1"},{"id":2,"name":"item 2"}]}が表示されるはずです。

loading.js

loading.jsはpage.jsが表示されるまでに表示するコンポーネントを記述します。loading.jsは最終的にlayout.jsの引き渡されるpage.jsをラップするSuspenseのfallbackに引き渡されるコンポーネントになります。loading.jsを追加して、ページのロード中の表示を追加してみましょう。

まずは、既存のpage.jsにタイマーを追加してリクエストからレスポンスまでに時間がかかるようにします。

// /app/tanesan/page.js

export default async function Page() {
  //3秒待つ
  await new Promise((resolve) => setTimeout(resolve, 3000));
  return <div>tanesan</div>;
}

同じ階層にloading.jsを配置します。

// /app/tanesan/loading.js

export default function Loading() {
  "Loading..."
}

/tanesanにアクセスしてみると、初めは「Loading…」が表示され、時間が経つと「tanesan」が表示されるはずです。

error.js

error.jsは配置されたディレクトリのページでエラーが発生した時に表示されるコンポーネントを記述します。こちらはエラーバウンダリのfallbackUIに引き渡されるコンポーネントのような感じでしょうか。こちらも試してみましょう。

まず、アクセスするとエラーをスローするページを作ります。

// /app/error/page.js

export default function Page() {
  throw new Error('Error Page!');
}

/errorにアクセスしてみましょう。「Unhandled Runtime Error」が画面に表示されるはずです。

続いて同じ階層にerror.jsを作成します。

// /app/error/page.js

'use client';

export default function Error({ error }) {
  return <div>{error.message}</div>;
}

error.jsページはクライアントコンポーネントになるので、'use client'をつけてください。
また、error.jsはpropsにスローされたエラーを受け取ることができます。
他にもページの再描画を試みるreset関数も受け取ることができます。

ファイルが作成できたら/errorにアクセスしてみましょう。page.jsでスローしたエラーのメッセージ、「Error Page!」が表示されているはずです。

page.jsの同階層にerror.jsがない場合は最も近いディレクトリのerror.jsにキャッチされます。
試しに/errorの配下にページを作ってみます。

// /app/error/error-child/page.js

export default function Page() {
  throw new Error('Error Child Page!');
}

/error/error-childにアクセスすると「Error Child Page!」が表示されているはずです。
これは、error-childのページで発生したエラーが親ディレクトリのerror.jsにキャッチされているためです。また、page.jsだけでなくlayout.jsで発生したエラーもキャッチすることができます。

global-error.js

error.jsでpage.jsと子階層のlayout.jsのエラー時の表示については対応することができますが、ルートレイアウトで発生したエラーについてはerror.jsでは対応できません。ルートレイアウトのエラーにはglobal-error.jsというファイルを使います。

使い方はerror.jsとほぼ同じですが、global-error.jsはルートレイアウトのエラー時のコンポーネントであるため、htmlとbodyタグが必要となります。

// /app/global-error.js

'use client';

export default function GlobalError({ error }) {
  return (
    <html>
      <body>
        <div>{error.message}</div>
      </body>
    </html>
  );
}

ルートレイアウトにエラーがあるとnpm run devで起動していた場合はすべてのページでエラーが表示されてしまいます。global-error.jsのエラーを確認するためにはビルド時にエラーが起こらないようにlayout.jsを構成し、npm run build && npm run startでアプリケーションを立ち上げる必要があります。ここでは詳細な表示方法は割愛します。詳しくはこちらのissue確認してください。

template.js

template.jsは役割的にはlayout.jsと似ており、ページ間で使いまわしたいUIを指定します。
layout.jsとの違いは、ページ遷移時に再描画を伴う点です。layout.jsではlayout.jsが配置されたディレクトリ配下のページに遷移する際はlayout.jsの内容は再描画されません。一方で、template.jsはページ遷移に伴い再描画されます。使い所としては、useEffectを使った処理、例えばページ単位でのロギングや、ページ単位のフィードバックフォームを使用する場合が考えられます。template.jsを使うことでページ遷移に伴いuseEffectの再実行やstateのリセットができるため有効です。また、template.jsとlayout.jsは同階層に配置することができます。この場合は、layout.jsがtemplate.jsをラップする形になります。

template.jsの動きを確認するために新しくページを作成します。

// /app/template/page.js

import Link from 'next/link';

export default function Page() {
  return (
    <>
      <div>Template Page</div>
      <Link href={'/template/template-child'}>go to child page</Link>
    </>
  );
}

layout.jsと子ページも作成します。

// /app/template/layout.js

'use client';

export default function Layout({ children }) {
  const randomString = Math.random().toString(36).substring(7);
  return (
    <>
      <div>{randomString}</div>
      <div>{children}</div>
    </>
  );
}

// /app/template/template-child/page.js

'use client';

import Link from 'next/link';

export default function Page() {
  return (
    <>
      <div>Child Template Page</div>
      <Link href={'/template'}>go to parent page</Link>
    </>
  );
}

/templateにアクセスしてからリンクをクリックして/template/template-childに遷移してみます。このとき、template配下のlayout.jsで生成したランダムな文字列はページ遷移しても変わらないことが確認できます。

次に、layout.jsと同階層にtemplate.jsを作成します。

// /app/template/template.js

'use client';

export default function Template({ children }) {
  const randomString = Math.random().toString(36).substring(7);
  return (
    <>
      <div>{randomString}</div>
      <div>{children}</div>
    </>
  );
}

/templateから/template-childに移動する際に、template.jsは再描画されるため、遷移するたびに文字列が変わっていることが確認できます。

not-found.js

not-found.jsはリクエストされたページがない場合に表示するUIを指定します。ページ内でnotFound関数を呼ぶことでそのページに最も近いnot-found.jsの内容が表示されます。
notFound関数はserver componentかserver actionsで使用する必要があります。

not-found.jsを使用した例です。

// /app/not-found/page.js

import { notFound } from 'next/navigation';

export default function Page() {
  return (
    <form
      action={async () => {
        'use server';
        notFound();
      }}
    >
      <button type="submit">Go to not found page</button>
    </form>
  );
}

// /app/not-found/not-found.js

export default function NotFound() {
  return <div>Not Found</div>;
}

not-foundページでは、server ationsを使ってボタンを押すとnotFound関数が呼び出されるようにします。ボタンを押すとnot-found.jsで指定した「Not Found」が表示されると思います。

おわりに

簡単にですが、Next.jsにおける特殊なファイルについてご紹介させていただきました。ここでざっと機能を理解してもらい、もし気になるものがあれば是非公式のドキュメントを確認してもらえればと思います!個人の感想ですが、Next.jsはかなりやれることが多いフレームワークで、使いこなすには結構勉強がいる気がします。一方で、一度使い方を覚えてしまえば今回ご紹介した内容も含め非常に便利な機能が多いため、複雑なWebアプリケーション開発においても高い生産性を出せる可能性を秘めています。自分もまだまだNext.jsについて知らないことが多いので、これからも継続的に勉強しつつ、今回のように学んだ内容をアウトプットしていけたらなと思っています!

AUTHOR
tanesan
tanesan
記事URLをコピーしました