In a Next.js App Router codebase, when is it worth implementing a modal as an intercepting route inside a @modal slot rather than as component state, and what does that choice cost the team?
answer
- does the content have an identity?
- shareable, bookmarkable, back-dismissable
- two routes rendering one component
- the cold path is the one that breaks
- transient UI stays in component state
basics
~20 sRoute the modal when its content is a real, shareable resource that deserves a URL, back-button dismissal and server-side data fetching. Keep it in component state when it is transient UI. The routed version costs extra files, a standalone page you must keep working, and fallback boilerplate.
solid answer
~60 sThe question to ask is whether the modal has an **identity**. If what it shows is a resource — a photo, an order, a profile — that someone would sensibly link to, bookmark, reach from search, or dismiss with the back button, then it deserves a URL, and the intercepting-route pattern gives it one without losing the context underneath. It also lets the modal's content be a Server Component that fetches its own data, so the feed does not have to preload every item's detail. If the modal is transient UI — a confirm prompt, a filter panel, an unsaved form — it has no identity worth putting in a URL, and routing it makes the back button do something users did not ask for. The cost is real: two routes rendering one view, a `default.tsx` per slot, a standalone page nobody exercises during development, close buttons that must navigate rather than set state, and a class of bugs that only appear on refresh or deep-link. I standardise one shared view component across both routes and reserve the pattern for genuinely addressable content.
go deeper
Take away the simple test: content someone would share a link to belongs in the URL; a confirm prompt does not. You are not expected to weigh the maintenance costs yet.
Be able to argue both sides concretely — name what the routed version buys (linkable URL, back-button dismissal, server-fetched detail) and what it costs (extra routes, fallback files, navigation-based dismissal).
Show you have paid the costs: the standalone page nobody exercises, the close button left as a state flip, the missing default.tsx that only breaks on refresh. Say how you would catch each one.
Own the convention for the codebase — which resources qualify, how the shared view component is structured so the two routes cannot drift, how dismissal is standardised, and what the onboarding cost of the pattern buys the product.
## The decision rule Strip away the folder conventions and one question remains: **does this modal show something with an identity?** A modal with identity displays a resource that exists independently of the fact that a dialog is open — a photo, an order, a user profile, a document. A modal without identity is a piece of interaction: confirm this, filter that, fill in these fields. The first kind belongs in the URL; the second does not. Everything else follows. If it belongs in the URL, users get link sharing, bookmarking, browser history, back-button dismissal, restoration after refresh, and — for public content — something a crawler can reach. If it does not belong in the URL, routing it produces a history entry nobody wanted, a back button that closes a dialog instead of leaving the page, and a URL that means nothing when pasted. ## What the routed version genuinely buys - **Addressability.** `/photos/12` is a real address whether the user arrived from the feed or from a message. That is the whole point of pairing interception with a standalone page. - **Server-rendered detail.** The intercepted route is a route, so its content can be a Server Component that fetches exactly the detail it needs. The list underneath does not have to over-fetch every item's full record so that a client dialog can render instantly. - **Isolation.** A slot has its own loading and error boundaries, so a slow or failing detail view degrades inside the dialog instead of taking the page with it. - **Back-button semantics that match user expectation** on mobile, where back is the primary dismiss gesture. ## What it costs - **Two routes, one view.** The intercepted route and the standalone page render the same content in different containers. Unless the content is factored into a single shared component, they drift — and the one that drifts is always the standalone page, because it never renders during ordinary development. - **Fallback boilerplate.** Every slot needs a `default.tsx`, and forgetting it turns into a 404 that only appears on a hard load. - **A second entry path to keep working.** The standalone page is the one users hit from shared links and search results, and the one nobody looks at. It needs its own test. - **Dismissal becomes navigation.** Escape, the backdrop click and the close button all have to trigger a router navigation. Any one of them left as a state flip leaves the URL pointing at a closed modal, so a refresh reopens it. - **A harder mental model.** A new engineer must understand slots, interception and the soft/hard navigation split before they can safely add a dialog. That is a real onboarding tax to levy on the whole codebase. ## How I would draw the line for a team 1. **Allow-list the pattern.** Name the resources that get URL-addressable overlays — typically the detail views that already have standalone pages for other reasons. Everything else uses a plain dialog component. 2. **One view component, two containers.** The shared detail component is the unit; the routes are thin wrappers. This makes the drift problem structurally impossible rather than a review responsibility. 3. **Pair the files.** Adding a slot and adding its `default.tsx` is one change. Make it a review checklist item, because its absence is silent. 4. **Test the cold path.** Whatever your end-to-end setup is, at least one check must load the standalone URL directly rather than clicking to it, because that is the path that breaks and the path real users take from shared links. 5. **Standardise dismissal.** One dialog wrapper that owns escape, the backdrop and the close control, and that navigates rather than setting state — so individual features cannot get it half right. ## The migration question The cheapest sequencing is to build the standalone page first and treat the overlay as an enhancement layered on top. A resource that does not deserve its own page almost certainly does not deserve an intercepting route either — which turns out to be a useful test of the original decision, applied before any of the folder conventions are written.
- What is the strongest argument against routing every modal in an app?Every routed modal adds a history entry, so the back button starts undoing dialogs rather than navigating. For transient UI that is actively wrong behaviour, and it comes on top of the extra files, the standalone page nobody exercises, and the onboarding cost of a convention that has to be understood before anyone can add a dialog.
- How do you stop the standalone page and the intercepted route from drifting apart?Make the view a single component and let both routes be thin wrappers — the page renders it directly, the intercepted route renders it inside the dialog shell. Structural sharing beats a review rule, because the standalone page is invisible during day-to-day development and no reviewer reliably remembers to open it.
- Does routing a modal change how its data is fetched?Yes, and it is one of the better reasons to do it. The routed content is its own route, so it can fetch exactly the detail it needs on the server. A client-state dialog usually forces the list underneath to over-fetch every item's full record, or to fire a client request on open with a spinner the routed version avoids.
saying these in an interview costs you the question
- Routes every dialog, including confirm prompts
- Assumes the pattern is free once the folders exist
- Keeps two hand-maintained copies of the view
- Thinks interception removes the need for a standalone page
- Ignores that a routed modal adds a history entry