Skip to the solution
HardFrontend8 min read

What are intercepting routes in Next.js and what are they used for?

271 views

The problem

Question presented to candidate: "In our photo feed, clicking a photo should open it in a modal over the feed, but the URL must change to /photo/123 so it can be shared, and opening that link directly should show a normal photo page. How do you build that in the App Router?"

What a strong answer should cover:

  • An intercepting route loads another route inside the current layout during a client navigation, while the URL changes to the real address.
  • Matchers in the folder name: (.) same level, (..) one level up, (..)(..) two levels up, (...) from the app root; they count route segments, not folders, so @slot and group folders do not count.
  • Interception only happens on soft navigation from inside the app. A reload, a direct visit or a shared link renders the real page.
  • The modal pattern combines it with a parallel route: @modal/(.)photo/[id]/page.tsx, plus @modal/default.tsx returning null.
  • Closing: router.back(), or a Link to a URL where the slot matches something that returns null (for example a catch-all in the slot).
  • The server decides interception from the Next-Url request header (the page the user is on), and the response varies on it.

Clarifying questions expected:

  • "Should a refresh keep the modal open?" β€” with interception it shows the full page instead; that is the intended behaviour.
  • "Where can the modal be opened from?" β€” decides the level of the matcher and where the slot lives.

Code / implementation expected: Yes β€” a feed layout with an @modal slot, an intercepted (.)photo/[id] page, the real photo/[id] page and a client Modal that closes with router.back().

  • nextjs
  • intercepting-routes
  • modals
  • routing

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

8 min read Β· 8 sections

Read the code

Photo modal with an @modal slot and an intercepted route
// app/feed/layout.tsx
// The @modal slot renders on top of the feed. It is a slot, not a URL segment.
export default function FeedLayout({ children, modal }: LayoutProps<"/feed">) {
  return (
    <>
      {children}
      {modal}
    </>
  );
}

// app/feed/page.tsx
import Link from "next/link";
export default function Feed() {
  return (
    <ul>
      {["1", "2", "3"].map((id) => (
        <li key={id}><Link href={`/feed/photo/${id}`}>Photo {id}</Link></li>
      ))}
    </ul>
  );
}

// app/feed/photo/[id]/page.tsx
// The real page: used on a direct visit, a shared link or a reload.
export default async function PhotoPage({ params }: PageProps<"/feed/photo/[id]">) {
  const { id } = await params;
  return <article><h1>Photo {id}</h1><p>Full page with comments, related photos and so on.</p></article>;
}

// app/feed/@modal/(.)photo/[id]/page.tsx
import Modal from "../../../../ui/Modal";

// (.) = intercept the photo segment that sits at the same level as this slot.
// Used only for soft navigations that start inside /feed.
export default async function PhotoModal({ params }: PageProps<"/feed/photo/[id]">) {
  const { id } = await params;
  return <Modal><h2>Photo {id}</h2></Modal>;
}

// app/feed/@modal/default.tsx
// Nothing in the modal slot by default (and after a reload). Required: builds fail without it.
export default function ModalDefault() {
  return null;
}

// app/feed/@modal/[...catchAll]/page.tsx
// Closes the modal when a Link inside /feed moves somewhere the slot has no match.
export default function CloseModal() {
  return null;
}

// app/ui/Modal.tsx
"use client";
import { useRouter } from "next/navigation";

export default function Modal({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  return (
    <div role="dialog" aria-modal="true" className="backdrop" onClick={() => router.back()}>
      <div className="panel" onClick={(e) => e.stopPropagation()}>
        {children}
        <button onClick={() => router.back()}>Close</button>
      </div>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 119 of 129 in Next.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Next.js questions
Up nextHard

`cacheLife` and `cacheTag` β€” how do you configure TTL profiles?

Continue