Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/next/src/build/static-paths/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,10 @@ export function resolveParallelRouteParams(
switch (paramType) {
case 'catchall':
case 'optional-catchall':
case 'catchall-intercepted':
case 'catchall-intercepted-(..)(..)':
case 'catchall-intercepted-(.)':
case 'catchall-intercepted-(..)':
case 'catchall-intercepted-(...)':
// If there are any non-parallel fallback route segments, we can't use the
// pathname to derive the value because it's not complete. We can make
// this assumption because routes are resolved left to right.
Expand Down Expand Up @@ -658,7 +661,10 @@ export function resolveParallelRouteParams(
break

case 'dynamic':
case 'dynamic-intercepted':
case 'dynamic-intercepted-(..)(..)':
case 'dynamic-intercepted-(.)':
case 'dynamic-intercepted-(..)':
case 'dynamic-intercepted-(...)':
// For regular dynamic parameters, take the segment at this depth
if (depth < pathSegments.length) {
const pathSegment = pathSegments[depth]
Expand Down
55 changes: 55 additions & 0 deletions packages/next/src/client/components/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, { useContext, useMemo, use } from 'react'
import {
AppRouterContext,
LayoutRouterContext,
GlobalLayoutRouterContext,
type AppRouterInstance,
} from '../../shared/lib/app-router-context.shared-runtime'
import {
Expand All @@ -17,6 +18,7 @@ import {
getSelectedLayoutSegmentPath,
} from '../../shared/lib/segment'
import { ReadonlyURLSearchParams } from './readonly-url-search-params'
import { extractRouteFromFlightRouterState } from './router-reducer/extract-route-from-flight-router-state'

const useDynamicRouteParams =
typeof window === 'undefined'
Expand Down Expand Up @@ -118,6 +120,59 @@ export function usePathname(): string {
return pathname
}

/**
* A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook
* that lets you read the canonical route structure including route groups, parallel routes, and dynamic parameters.
*
* Unlike `usePathname()` which returns the actual URL path, `useRoute()` returns the file-system structure
* of the route, preserving route groups `(group)`, parallel routes `@slot`, and dynamic parameters `[param]`. When
* an intercepted route is active, the route will be the path of the intercepted route.
*
* @example
* ```ts
* "use client"
* import { useRoute } from 'next/navigation'
*
* export default function Page() {
* const route = useRoute()
* // On /blog/my-post, returns "/blog/[slug]"
* // On /dashboard with @modal slot active, returns "/dashboard/@modal/..."
* // With route group (marketing), returns "/(marketing)/about"
* // ...
* }
* ```
*/
// Client components API
export function useRoute(): string {
useDynamicRouteParams?.('useRoute()')

const pathname = useContext(PathnameContext)
const globalContext = useContext(GlobalLayoutRouterContext)
const tree = globalContext?.tree

// Compute the canonical route from the tree
// The tree structure itself represents the active route state,
// so we just traverse it to build the canonical path
// Memoized to avoid expensive tree traversal on every render
const route = useMemo(() => {
if (!tree || !pathname) {
return '/'
}
return extractRouteFromFlightRouterState(pathname, tree) ?? '/'
}, [pathname, tree])

// Instrument with Suspense DevTools (dev-only)
if (process.env.NODE_ENV !== 'production' && 'use' in React) {
const navigationPromises = use(NavigationPromisesContext)
if (navigationPromises) {
// TODO: Add instrumented promise for route if needed for DevTools
// For now, return the computed value directly
}
}

return route
}

// Client components API
export {
ServerInsertedHTMLContext,
Expand Down
Loading
Loading