Intercepting Routes are triggered by hash/query-only URL changes — is this intended? #90172
Replies: 2 comments
|
This is a known quirk of how Next.js App Router handles intercepting routes. The interception matching checks the full URL (including search params) against the intercepting route pattern, and any navigation (including Workaround: Detect whether navigation is an interception or an in-page update Use 'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, useRef } from 'react';
export function useIsInterceptedNavigation() {
const pathname = usePathname();
const prevPathname = useRef(pathname);
useEffect(() => {
prevPathname.current = pathname;
}, [pathname]);
return prevPathname.current !== pathname;
}Then in your intercepting route's export default function InterceptedPage() {
const isIntercepted = useIsInterceptedNavigation();
if (!isIntercepted) return null; // Skip for hash/query-only changes
return <Modal />;
}Alternative: Use This behavior is arguably a bug — interception should ideally only trigger on pathname segment changes, not query/hash mutations. Worth opening an issue on the Next.js repo if you haven't already. |
|
To answer your core question: yes, this is the current behavior by design, but it's widely considered a rough edge of the Intercepting Routes implementation. Here's a clearer mental model and the most practical workarounds: Why it happensNext.js App Router's Intercepting Routes matching runs on every navigation event — the router doesn't distinguish between "pathname changed" and "only hash/search changed". Every call to The interception condition ( Workaround 1: Suppress hash/anchor navigation through the routerInstead of // ❌ Triggers router navigation → activates intercepting route
<a href="#section">Go to section</a>
// ✅ Scrolls without router involvement
<button
onClick={() => document.getElementById('section')?.scrollIntoView({ behavior: 'smooth' })}
>
Go to section
</button>This avoids the interception entirely because no navigation event fires. Workaround 2: Guard the intercepting route with
|
| Workaround | Handles hash? | Handles search params? | Complexity |
|---|---|---|---|
Scroll via getElementById |
✅ Yes | N/A | Low |
usePathname mount guard |
✅ Yes | ✅ Yes | Low |
router.replace + scroll:false |
N/A | Partial | Low |
sessionStorage intent flag |
✅ Yes | ✅ Yes | Medium |
Uh oh!
There was an error while loading. Please reload this page.
Context
While working with App Router + Parallel Routes + Intercepting Routes, I ran into a behavior that seems unintuitive and difficult to work around in real-world apps.
Intercepting Routes appear to be re-evaluated on any URL change, including changes limited to:
hash (#section)
search params (?tab=1)
even when the pathname does not change.
This causes parallel route slots to activate unintentionally.
Example scenario
A route exists in two forms:
/menu → real page (SEO, direct access, indexable)
@overlay/(.)menu → intercepting route used to render an overlay/modal
This is a recommended pattern to support:
SEO-safe routes
enhanced UX with overlays
However:
User accesses /menu directly (full page)
Inside /menu, clicks an anchor link (#section) or a link that only updates search params
The router treats this as a navigation
The intercepting route (.)menu is matched
The overlay slot becomes active
Result: duplicated UI (page + overlay)
No pathname change occurred, yet interception was triggered.
Why this feels problematic
From a developer and UX perspective:
Hash navigation is traditionally a DOM concern, not a routing concern
Anchor links inside a page should not activate overlays
This makes it unsafe to combine:
real pages
intercepting routes
internal anchors or query-based UI state
It forces developers to either:
avoid hash/query navigation entirely
or implement workarounds (sessionStorage flags, custom click handlers, router guards)
None of which feel aligned with the mental model of Intercepting Routes.
Expected behavior (question)
Is the current behavior intentional?
Intuitively, intercept matching would be expected to occur only when:
the pathname changes
or when navigation explicitly targets an intercepted route
Hash-only or search-only updates would ideally not trigger intercept evaluation.
Minimal structure
app/
├─ menu/
│ └─ page.tsx
├─ @overlay/
│ ├─ default.tsx
│ └─ (.)menu/
│ └─ page.tsx
Steps to reproduce:
Open /menu directly
Trigger a hash (#section) or search param change
Observe the overlay slot activating
Open questions
Is this behavior by design?
Are Intercepting Routes expected to react to hash/search changes?
Is there a recommended way to:
distinguish intentional intercept navigation
from in-page URL updates?
Would limiting intercept matching to pathname changes be feasible?
Closing
This isn’t a complaint — just trying to understand the intended mental model.
Right now, combining:
SEO-safe routes
intercepting overlays
and anchor navigation
feels fragile, and it’s unclear whether this is a known limitation or an area that might evolve.
Any clarification or guidance would be greatly appreciated.
All reactions