Next.js App Router involving parallel routes, intercepting routes, and server components #94505
-
SummaryI'm facing an issue with the Next.js App Router involving parallel routes, intercepting routes, and server components. I have the following structure: app/
├─ dashboard/
│ ├─ page.tsx
│ ├─ @analytics/
│ │ └─ page.tsx
│ ├─ @notifications/
│ │ └─ page.tsx
│
├─ feed/
│ └─ page.tsx
│
├─ photo/
│ └─ [id]/
│ └─ page.tsx
│
├─ @modal/
│ └─ (.)photo/
│ └─ [id]/
│ └─ page.tsxThe goal is:
ProblemWhen navigating between photos:
Questions
EnvironmentNext.js: Latest
App Router: Enabled
React: 19I would appreciate any insights into how App Router caching and route segment preservation work internally in this scenario. |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments
-
|
The issue is most likely caused by how App Router handles route segment preservation during intercepted navigation. When using intercepting routes ( A few things helped resolve similar behavior: 1. Move Data Fetching Higher in the TreeIf analytics or notification data is fetched inside components that belong to a route segment affected by navigation, those components may re-render. Instead, fetch shared data in a parent layout: app/dashboard/layout.tsxand pass it down to child components. This allows Next.js to reuse the layout cache during intercepted navigation. 2. Ensure Stable Layout HierarchyParallel routes are preserved only when the parent layout remains mounted. Example: dashboard
├── layout.tsx
├── @analytics
├── @notificationsIf navigation causes Keep intercepting routes outside of segments that own persistent state whenever possible. 3. Use Intercepting Routes Only for UI OverlayThe intercepted route should contain only modal-specific UI. Avoid putting expensive data fetching directly inside the modal route. Instead: feed
→ fetch data
modal
→ reuse existing dataThis reduces unnecessary server execution. 4. Cache Server FetchesFor server-side requests: fetch(url, {
next: {
revalidate: 60
}
})or import { cache } from "react"This prevents repeated execution when components re-render. 5. Verify Component ReuseA useful debugging technique is: console.log("Layout rendered")inside: layout.tsxand page.tsxIf only the page logs appear during navigation, the layout is being reused correctly. If the layout logs again, the segment is being remounted. 6. Recommended StructureFor modal-based navigation: app/
├── feed/
│ └── page.tsx
│
├── photo/[id]/page.tsx
│
├── @modal/
│ └── (.)photo/[id]/page.tsxKeep modal routes isolated from dashboard layouts and parallel route owners. This allows:
In practice, most state loss issues with App Router, Parallel Routes, and Intercepting Routes come from parent layout remounts rather than the modal route itself. Ensuring a stable layout hierarchy and moving shared data fetching into persistent layouts usually resolves the problem. |
Beta Was this translation helpful? Give feedback.
-
|
I think there are two separate things getting mixed together here: React re-rendering, and App Router segment preservation. In your specific tree, app/
├─ dashboard/
├─ feed/
├─ photo/
└─ @modal/The The first thing I would check is: where is the layout that renders For example, if your modal is intended to work globally from // app/layout.tsx
export default function RootLayout({
children,
modal,
}: LayoutProps<'/'>) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
)
}with: app/
├─ layout.tsx
├─ @modal/
│ ├─ default.tsx
│ └─ (.)photo/
│ └─ [id]/
│ └─ page.tsx
├─ feed/
│ └─ page.tsx
├─ photo/
│ └─ [id]/
│ └─ page.tsx
└─ dashboard/
├─ layout.tsx
├─ page.tsx
├─ @analytics/
│ └─ page.tsx
└─ @notifications/
└─ page.tsxIn that setup, navigating from If you want feed + modal + analytics + notifications to all persist together, then those slots need to be owned by a common layout above the routes that change, for example at the root or inside a shared route group. If the slots stay under On refresh, the behavior is different. Intercepting routes apply to soft navigation. If the user directly loads or refreshes app/photo/[id]/page.tsxnot the intercepted modal route: app/@modal/(.)photo/[id]/page.tsxThat part is expected. For React itself: yes, if the tree identity changes, everything below that point can be recreated. But it is not quite true that every parent re-render discards all children. React can preserve child state when the component type, position, and key are stable. However, if a parent changes from one layout/component boundary to another, or a key changes, or an App Router segment is replaced, then the subtree below that point can remount. So in App Router terms:
For avoiding unnecessary refetching, I would structure it so that stable data lives in stable layouts, and photo-specific data lives only in the photo modal/page segment. For example: app/
├─ layout.tsx // global shell, modal slot
├─ feed/page.tsx // feed data
├─ @modal/default.tsx
├─ @modal/(.)photo/[id]/page.tsx // modal photo data only
└─ photo/[id]/page.tsx // full-page photo data onlyAvoid putting dynamic reads like To debug it, I would add logs at each boundary: // app/layout.tsx
console.log('render root layout')
// app/feed/page.tsx
console.log('render feed page')
// app/@modal/(.)photo/[id]/page.tsx
console.log('render intercepted photo modal', params.id)
// app/photo/[id]/page.tsx
console.log('render full photo page', params.id)
// app/dashboard/layout.tsx
console.log('render dashboard layout')Then test these separately:
If the root layout logs again on client navigation, something above the modal boundary is changing identity. If only the modal page logs, the routing structure is probably correct. If dashboard logs disappear, that is expected when leaving
Because with the tree shown, they are dashboard-scoped. If they need to persist across |
Beta Was this translation helpful? Give feedback.
The issue is most likely caused by how App Router handles route segment preservation during intercepted navigation.
When using intercepting routes (
(.)) together with parallel routes (@slot), Next.js will only preserve layouts and Server Components if the route segment hierarchy remains unchanged. If a parent segment changes, Next.js may invalidate part of the router cache and remount the affected subtree.A few things helped resolve similar behavior:
1. Move Data Fetching Higher in the Tree
If analytics or notification data is fetched inside components that belong to a route segment affected by navigation, those components may re-render.
Instead, fetch shared data in a parent layout: