🐈‍⬛

【Next.js】Intercepting Routes + Parallel RoutesでModalを扱う

2024/09/06に公開

はじめに

Next.jsのApp Routerが使えるようになってしばらく経ちますが、Intercepting Routes + Parallel Routesを使ったModalの扱いがとても便利だったので、こちらに残したいと思います。

やりたいこと

シンプルなToDoアプリを今回の対象とします。

この時、以下のような挙動を満たしたいとします。

  1. 一覧にあるいずれかのToDoをクリックするとToDoの編集ダイアログが開く。更新やキャンセルを押す、またはブラウザバックすることで、このダイアログは閉じて元の画面に戻る

  1. 編集ダイアログを開いた時のURLに直接アクセスした場合、ダイアログではなく編集ページが開く

こういったケースを実現する際に、Intercepting Routes + Parallel Routesを使った方法が非常に便利だったため、ここで紹介したいと思います。

Parallel Routes

Parallel RoutesはNext.jsのApp Routerで利用できる機能となります。

https://nextjs.org/docs/app/building-your-application/routing/parallel-routes

この機能を利用することで、現在のページの状態を維持しつつ、サブページ(Slot)としてModalを開くことが可能となります。別の言い方をすると、/todo/[i]というパスに遷移した時、/todo/page.tsxの表示状態を維持したまま、/todo/[id]/page.tsxの内容を表示させることが可能となります。

この機能を用いることで、Modalを開いた時の状態をURLパスとして保持することが可能となります。すなわち、ブラウザバックやブラウザフォワードでModalを開閉することが可能になります。
但し、この機能のみだとModal表示中にリロードした際、元の状態が再現できなくなります(※)。リロードや直リンクでアクセスした際に、編集画面に遷移させるためには次のIntercepting Rotes利用します。

※リロードなどのHard Navigationでは、/todo/page.tsxの状態が失われるので、/todo/default.tsx or 404にフォールバックされる

Parallel Routesの詳細については、別の記事でも紹介していますのでご参照ください🙇

https://zenn.dev/mktu/articles/abb79de95d402c

Intercepting Routes

Intercepting Routesも、Next.jsのApp Routerで利用できる機能となります。

https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes

こちらは、Linkなどで遷移した際に、本来表示されるべきページとは別のページを表示させることが可能になります。具体的には、(.)[id]のようなディレクトリを/todo/[id]と同じ階層に置くことで/todoページなどから/todo/[id]に遷移した時に、/todo/[id]/page.tsxではなく、/todo/(.)[id]/page.tsxがrenderされることになります。

app/todo
├── page.tsx
├── [id]
│     ├── page.tsx
├── (.)[id]
│     ├── page.tsx ←/todo/[id]をinterceptする

※ (.)はInterceptする対象が同じパス階層(ディレクトリ階層ではない)にあることを示します。一つ上の階層をInterceptしたい場合(..)を用います

一度/todo/(.)[id]/page.tsxがrenderされた後にページのリロードや直接アクセスを行うと、/todo/[id]/page.tsxがrenderされるようになります。つまり今回の場合、リロードすると非Modalの編集ページが表示されることになります。

実装

上で紹介した2つの機能を用いて、今回のModalを実装していきたいと思います。
まず、ファイル構成としては以下のようにファイルを用意します。

app/todo
├── page.tsx
├── layout.tsx
├── [id]
│     ├── page.tsx
└──── @modal
      ├── (.)[id]
      │     └─── page.tsx
      └──── default.tsx

/todo以下の各ファイルの役割の概要は以下のとおりです。

ファイル 役割
layout.tsx Parallel Routesを利用するため、page.tsxと@modalのSlotをrenderする
[id]/page.tsx 非Modal用のToDo編集ページ。直接アクセスされた際に表示
@modal/(.)[id]/page.tsx Modal用のToDo編集ページ。ページ内のリンク経由でアクセスされた際に表示
@modal/default.tsx @modal Slotがパスマッチしない時に表示するサブページ。Modalが開かれていないときは基本的に何も表示しないので、基本的にnullを返すだけのコンポーネント

以下では、各ファイルのポイントについて概説したいと思います。

layout.tsx

@modalのSlotをlayout.tsxにてrenderします。

type Props = {
    children: React.ReactNode;
    modal: React.ReactNode;
}
const Layout: FC<Props> = ({ children, modal }) => {
    return (
        <div className='w-screen'>
            {children}
            {modal}
        </div>
    );
}

これでParallel Routeによって、同じレイアウト内で/todo, /@modal/**をrenderすることが可能になります。

@modal/(.)[id]/page.tsx

Modalで表示する際のページになります。

...
const EditTodoModal: FC<Props> = ({ todo }) => {
    const router = useRouter();
    return (
        <Dialog
            modal
            open
            onOpenChange={(isOpen) => {
                if (!isOpen) {
                    router.back()
                }
            }}
        >
            <DialogContent>
                ...
            </DialogContent>
        </Dialog>
    );
}

const Page: FC<Props> = async ({ params }) => {
    const todo = await getTodo(Number(params.id))
    return <EditTodoModal todo={todo} />;
}

export default Page;

ポイントとしては、Dialogについてはopen状態のstateを持っておらず、初回レンダー時にopen(=true)を渡しています。Modalを閉じる時には、open=falseにするのではなく、router.back()する必要があります。

[id]/page.tsx

こちらは、非Modalの編集ページになります。通常のNext.jsのpageコンポーネントと同様に実装する形で問題ありません。

const Page: FC<Props> = async ({ params }) => {
    const todo = await getTodo(Number(params.id))
    return (
        <form className='max-w-[700px] p-10 flex flex-col gap-2'>
            ...formの中身を実装
        </form>
    );
}
export default Page;

@modal/default.tsx

こちらはSlotがパスマッチしない時に表示されるページとなります。具体的には、Modalを開いていない時=パスが/todo/todo/[id]の時に表示されることになります。
基本的には、何も表示する必要はないためnullを返します。

export default function Default() {
    return null
}

ポイントとなる実装は以上となります。

最後に

ここまで読んでいただきありがとうございます。
今回紹介したコードの完全版については以下で公開しています。
(今回はModalの挙動確認が目的のため、更新処理などは実装していません)

https://github.com/mktu/nextjs-modal-sample

Discussion