From 9ae437f4b1212bf52bd4f4a5540a557b546db3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Thu, 29 Feb 2024 14:14:52 +0100 Subject: [PATCH] fix(navigation): allow `useSelectedLayoutSegment(s)` in Pages Router (#62584) ### What? Do not fail when `useSelectedLayoutSegment` or `useSelectedLayoutSegments` APIs are called in the Pages Router. ### Why? This makes migration easier and creates consistency with our other App Router-specific APIs that inherit the same behavior. ### How? Similar to #47490, we return `null` if there is no Layout context (indicating being in Pages Router) Types are also overridden in the navigation compact module declaration which kicks in during start to correct the types if we detect a `pages/` directory. Note to reviewer: #47490 didn't add a test, so I added one top-level, let me know if you have a better suggestion for placing. Closes NEXT-2506 Fixes #61464 --- .../next/navigation-types/compat/navigation.d.ts | 16 ++++++++++++++++ .../next/src/client/components/navigation.ts | 10 +++++++--- .../lib/app-router-context.shared-runtime.ts | 3 ++- .../pages/index.tsx | 10 ++++++++++ ...lectedlayoutsegment-s-in-pages-router.test.ts | 14 ++++++++++++++ 5 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 test/e2e/useselectedlayoutsegment-s-in-pages-router/pages/index.tsx create mode 100644 test/e2e/useselectedlayoutsegment-s-in-pages-router/useselectedlayoutsegment-s-in-pages-router.test.ts diff --git a/packages/next/navigation-types/compat/navigation.d.ts b/packages/next/navigation-types/compat/navigation.d.ts index e2e225d2e664d..6a1f925a1b908 100644 --- a/packages/next/navigation-types/compat/navigation.d.ts +++ b/packages/next/navigation-types/compat/navigation.d.ts @@ -31,4 +31,20 @@ declare module 'next/navigation' { string | string[] > >(): T | null + + /** + * A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook + * that lets you read the active route segments **below** the Layout it is called from. + * + * If used from `pages/`, the hook will return `null`. + */ + export function useSelectedLayoutSegments(): string[] | null + + /** + * A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook + * that lets you read the active route segment **one level below** the Layout it is called from. + * + * If used from `pages/`, the hook will return `null`. + */ + export function useSelectedLayoutSegment(): string | null } diff --git a/packages/next/src/client/components/navigation.ts b/packages/next/src/client/components/navigation.ts index f666de69777f9..47644bb5ffe9f 100644 --- a/packages/next/src/client/components/navigation.ts +++ b/packages/next/src/client/components/navigation.ts @@ -211,8 +211,11 @@ function useSelectedLayoutSegments( parallelRouteKey: string = 'children' ): string[] { clientHookInServerComponentError('useSelectedLayoutSegments') - const { tree } = useContext(LayoutRouterContext) - return getSelectedLayoutSegmentPath(tree, parallelRouteKey) + const context = useContext(LayoutRouterContext) + // @ts-expect-error This only happens in `pages`. Type is overwritten in navigation.d.ts + if (!context) return null + + return getSelectedLayoutSegmentPath(context.tree, parallelRouteKey) } /** @@ -238,7 +241,8 @@ function useSelectedLayoutSegment( ): string | null { clientHookInServerComponentError('useSelectedLayoutSegment') const selectedLayoutSegments = useSelectedLayoutSegments(parallelRouteKey) - if (selectedLayoutSegments.length === 0) { + + if (!selectedLayoutSegments || selectedLayoutSegments.length === 0) { return null } diff --git a/packages/next/src/shared/lib/app-router-context.shared-runtime.ts b/packages/next/src/shared/lib/app-router-context.shared-runtime.ts index b02c4993b586e..580219325668f 100644 --- a/packages/next/src/shared/lib/app-router-context.shared-runtime.ts +++ b/packages/next/src/shared/lib/app-router-context.shared-runtime.ts @@ -148,7 +148,8 @@ export const LayoutRouterContext = React.createContext<{ childNodes: CacheNode['parallelRoutes'] tree: FlightRouterState url: string -}>(null as any) +} | null>(null) + export const GlobalLayoutRouterContext = React.createContext<{ buildId: string tree: FlightRouterState diff --git a/test/e2e/useselectedlayoutsegment-s-in-pages-router/pages/index.tsx b/test/e2e/useselectedlayoutsegment-s-in-pages-router/pages/index.tsx new file mode 100644 index 0000000000000..dfe7150183b3c --- /dev/null +++ b/test/e2e/useselectedlayoutsegment-s-in-pages-router/pages/index.tsx @@ -0,0 +1,10 @@ +import { + useSelectedLayoutSegment, + useSelectedLayoutSegments, +} from 'next/navigation' + +export default function Page() { + useSelectedLayoutSegment() + useSelectedLayoutSegments() + return

Hello World

+} diff --git a/test/e2e/useselectedlayoutsegment-s-in-pages-router/useselectedlayoutsegment-s-in-pages-router.test.ts b/test/e2e/useselectedlayoutsegment-s-in-pages-router/useselectedlayoutsegment-s-in-pages-router.test.ts new file mode 100644 index 0000000000000..211faa8f1b553 --- /dev/null +++ b/test/e2e/useselectedlayoutsegment-s-in-pages-router/useselectedlayoutsegment-s-in-pages-router.test.ts @@ -0,0 +1,14 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('useSelectedLayoutSegment(s) in Pages Router', () => { + const { next } = nextTestSetup({ files: __dirname }) + + it('Should render with `useSelectedLayoutSegment(s) hooks', async () => { + const browser = await next.browser('/') + + await browser.waitForElementByCss('#hello-world') + expect(await browser.elementByCss('#hello-world').text()).toBe( + 'Hello World' + ) + }) +})