samurai japan warrior

Next.js の Interception Routes について

Intercepting routes は Next.js 13.3 から追加された機能で、App Router(app ディレクトリ)において使用できます。Intercepting routes ではブラウザの URL を遷移先のものに上書きしつつ、現在のレイアウトに新しいページを表示できます。これは例えば Instagram のように、ユーザーのプロフィールから写真をクリックすると、写真をモーダルを開き、ページを更新したり共通したりするとデフォルトのレイアウトで表示する場合などに便利です。

Intercepting routes は Next.js 13.3 から追加された機能で、App Router(app ディレクトリ)において使用できます。

Intercepting routes ではブラウザの URL を遷移先のものに上書きしつつ、現在のレイアウトに新しいページを表示できます。これは例えば以下のようなケースで便利です。

  • Instagram のように、ユーザーのプロフィールから写真をクリックすると、写真をモーダルを開き、ページを更新したり共通したりするとデフォルトのレイアウトで表示する場合
  • タスクの一覧を表示しつつ、新しいタスクを作成・更新するフォームをモーダルで表示したい場合

使い方

Intercepting routes により「横取り」したいページは先頭に (..) をつけます。ちょうど相対パスと ../ と同じような感じです。また、(...) を使用すると app ディレクトリからの相対パスとなります。これが Interception routes の規約です。

Vercel より提供されている nextgram というサンプルアプリケーションを見てみましょう。

このサンプルアプリケーションでは、写真の一覧から写真をクリックするとモーダルで写真を表示します。このとき、URL は /photos/1 に変更されます。この状態でページを更新してみると、モーダルではなくデフォルトのレイアウトで写真が表示されます。

app ディレクトリ内は以下のようになっています。

app
├── @modal
│   ├── (..)photos
│   │   └── [id]
│   │       └── page.js
│   └── default.js
├── default.js
├── global.css
├── layout.js
├── opengraph-image.png
├── page.js
└── photos
    └── [id]
        └── page.js

@modal/(..)photos/[id]/page.js の箇所が Interception routes です。このファイルの内容が、/photos/1 のようなルートにクライアントサイドで遷移したときに描画されます。

app/@modal/(..)photos/[id]/page.js
import Photo from "../../../../components/frame";
import Modal from "../../../../components/modal";
import swagPhotos from "../../../../photos";
 
export default function PhotoModal({ params: { id: photoId } }) {
  const photos = swagPhotos;
  const photo = photoId && photos.find((p) => p.id === photoId);
 
  return (
    <Modal>
      <Photo photo={photo} />
    </Modal>
  );
}

<Modal> を閉じる際には router.back() を使用して前のページに戻るようになっていることがわかります。これはモーダルが表示される時に、/photos/1 という URL に遷移することになるからです。

components/modal/index.js
"use client";
import { useRouter } from "next/navigation";
 
export default function Modal({ children }) {
  const router = useRouter();
 
  const onDismiss = useCallback(() => {
    router.back();
  }, [router]);

Parallel Routes

ディレクトリは @modal と先頭に @ が付けられた名前から始まっています。これも Next.js 13.3 から追加された機能で Parallel Routes と呼ばれています。

Parallel Routes は同じレイアウトの中に複数のページを表示するための機能です。この機能を利用して、写真の一覧とモーダルを同時に表示しています。

Parellel Routes は名前付きスロットにより生成されます。規約では @modal のように先頭に @ が付けられた名前がスロットの名前として使用されます。この @modal スロットは同じ階層にある Layout コンポーネントの Props として受け取ります。

app/layout.js
import "./global.css";
import GithubCorner from "../components/github-corner";
 
export default function Layout(props) {
  return (
    <html>
      <body>
        <GithubCorner />
        {/*  通常の children props */}
        {props.children}
        {/* `@modal` ディレクトリが描画される */}
        {props.modal}
      </body>
    </html>
  );
}

ここで、children props は暗黙的に解決されます。つまり、app/page.jsapp/@children/page.js と同等ということです。ディレクトリの @modal の部分は URL には影響を与えず、/photos/1 のような URL として扱われます。

上記の layout.js の例では /photos/1 という URL にアクセスした場合には app/page.jsapp/@modal/photos/[id]/page.js の両方の内容が描画されることになります。

写真一覧ページ(app/page.js

page.js の内容も見てみましょう。モーダルのトリガーとして、<Link> コンポーネントを使用していることがわかります。/photos/1 という URL にページ遷移を発生させることにより、Parallel Routes として定義した app/@modal/photos/[id].page.js のモーダルが表示されます。

app/page.js
import Link from "next/link";
import swagPhotos from "../photos";
import Image from "next/image";
 
export default function Home() {
  const photos = swagPhotos;
 
  return (
    <main className="container mx-auto">
      <h1 className="text-center text-4xl font-bold m-10">NextGram</h1>
      <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 auto-rows-max	 gap-6 m-10">
        {photos.map(({ id, imageSrc }) => (
          <Link key={id} href={`/photos/${id}`}>
            <Image
              alt=""
              src={imageSrc}
              height={500}
              width={500}
              className="w-full object-cover aspect-square"
            />
          </Link>
        ))}
      </div>
    </main>
  );
}

デフォルトのレイアウト

ところで、/photos/1 という URL には app/photos/[id].tsx というファイルにもマッチします。

app/photos/[id].tsx
import React from "react";
import Photo from "../../../components/frame";
import swagPhotos from "../../../photos";
 
export default function PhotoPage({ params: { id } }) {
  const photo = swagPhotos.find((p) => p.id === id);
 
  return (
    <div className="container mx-auto my-10">
      <div className="w-1/2 mx-auto border border-gray-700">
        <Photo photo={photo} />
      </div>
    </div>
  );
}

このクライアントで遷移した時に app/photos/[id].tsx ではなく app/@modal/photos/[id].tsx が優先して解決されるからこそ、Interception routes という名前なのです。

クライアントサイド以外での遷移、例えば /photos/1 に直接アクセスした場合には、app/photos/[id].tsx が解決されます。これにより、写真一覧から遷移したときにはモーダルが、ページを更新したときには写真の詳細ページが表示される機能が実現されています。

参考


Contributors

> GitHub で修正を提案する
この記事をシェアする
はてなブックマークに追加

関連記事