-
Notifications
You must be signed in to change notification settings - Fork 26.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix useSelectedLayoutSegment's support for parallel routes (#60912)
fixes NEXT-2173 Fixes #59968 ### TODOs - [x] recreate [repro](https://github.com/williamli/nextjs-NEXT-2173) - [x] patch `useSelectedLayoutSegment` to support parallel routes (see "What") - [x] check `useSelectedLayoutSegments` to see if it is affected - [x] add test cases - [x] finalise PR description ### What? `useSelectedLayoutSegment` does not return the name of the active state of parallel route slots. #### Expected Behaviour According to https://nextjs.org/docs/app/building-your-application/routing/parallel-routes#useselectedlayoutsegments > When a user navigates to app/@auth/login (or /login in the URL bar), loginSegments will be equal to the string "login". 👉🏽 We should update the docs to explain `null` and __DEFAULT__ result as well. According to the [API reference for useSelectedLayoutSegment](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment#returns): > useSelectedLayoutSegment returns a string of the active segment or null if one doesn't exist. > For example, given the Layouts and URLs below, the returned segment would be: > <img width="881" alt="CleanShot 2024-01-20 at 14 50 52@2x" src="https://github.com/vercel/next.js/assets/179761/bfaa34c8-3139-4ec3-bd70-4346c682e36b"> #### Current Behaviour Currently a string "children" is returned for everything inside a parallel route with active state and `__DEFAULT__` is returned if there is no active state for the parallel route (since the `default.tsx` is loaded). ~`null` is returned when the `default.tsx` is not loaded (possibly caused by another bug, see test case 5).~ #### Reproduction [GitHub Repo](https://github.com/williamli/nextjs-NEXT-2173) is created based on the example provided in [Next.js docs for using `useSelectedLayoutSegment` with Parallel Routes](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes#useselectedlayoutsegments). #### Test Cases 1. If you visit https://next-2173.vercel.app/, you get loginSegments: __DEFAULT__ (hard navigation) or children (soft navigation after returning from a visit to /login) 2. If you soft nav to (/app/@auth/login and /app/@nav/login) https://next-2173.vercel.app/login, you get 1. loginSegment: `children` (expected value should be `login`) 2. navSegment: `children` (expected value should be `login`) 3. If you soft nav to (/app/@auth/reset) https://next-2173.vercel.app/reset, you get 1. loginSegments: `children` (expected value should be `reset`) 2. navSegment: `children` (expected value should be `login`) 4. If you soft nav to (/app/@auth/reset/withEmail) https://next-2173.vercel.app/reset/withEmail, you get 1. loginSegments: `children` (expected value should be `withEmail`) 2. navSegment: `children` (expected value should be `login`) 5. ~If you hard nav to (/app/@auth/reset/withEmail) https://next-2173.vercel.app/reset/withEmail, you get an unexpected result due to possibly another bug:~ * ~navSegment is `null` on the deployed (Vercel) version, the navSlot is *not* loaded~ * ~navSegment is `__DEFAULT__` on local dev, the navSlot loads `/app/@nav/default.tsx`.~ ### Why? In `packages/next/src/client/components/navigation.ts`, `getSelectedLayoutSegmentPath` is called and returns the correct segmentPath for parallel routes (even though there is a TODO comment indicating this function needs to be updated to handle parallel routes) but `useSelectedLayoutSegment` failed to return the correct segment when a parallelRouteKey is provided. ### How? `useSelectedLayoutSegment` is updated to return selectedLayoutSegments[0] for non parallel routes (original logic), but it will return the last segments for parallel routes (or null if nothing is active). ``` return parallelRouteKey === 'children' ? selectedLayoutSegments[0] : selectedLayoutSegments[selectedLayoutSegments.length-1] ?? null ``` --------- Co-authored-by: Zack Tanner <zacktanner@gmail.com>
- Loading branch information
Showing
15 changed files
with
178 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 3 additions & 0 deletions
3
test/e2e/app-dir/parallel-routes-use-selected-layout-segment/app/@auth/default.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return <div>/app/@auth/default.tsx</div> | ||
} |
3 changes: 3 additions & 0 deletions
3
test/e2e/app-dir/parallel-routes-use-selected-layout-segment/app/@auth/login/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return <div>/app/@auth/login/page.tsx</div> | ||
} |
3 changes: 3 additions & 0 deletions
3
test/e2e/app-dir/parallel-routes-use-selected-layout-segment/app/@auth/reset/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return <div>/app/@auth/reset/page.tsx</div> | ||
} |
3 changes: 3 additions & 0 deletions
3
...2e/app-dir/parallel-routes-use-selected-layout-segment/app/@auth/reset/withEmail/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return <div>/app/@auth/reset/withEmail/page.tsx</div> | ||
} |
3 changes: 3 additions & 0 deletions
3
...e/app-dir/parallel-routes-use-selected-layout-segment/app/@auth/reset/withMobile/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return <div>/app/@auth/reset/withMobile/page.tsx</div> | ||
} |
3 changes: 3 additions & 0 deletions
3
test/e2e/app-dir/parallel-routes-use-selected-layout-segment/app/@nav/default.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return <div>/app/@nav/default.tsx</div> | ||
} |
3 changes: 3 additions & 0 deletions
3
test/e2e/app-dir/parallel-routes-use-selected-layout-segment/app/@nav/login/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return <div>/app/@nav/login/page.tsx</div> | ||
} |
3 changes: 3 additions & 0 deletions
3
test/e2e/app-dir/parallel-routes-use-selected-layout-segment/app/default.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return <div>/app/default.tsx</div> | ||
} |
Binary file added
BIN
+25.3 KB
test/e2e/app-dir/parallel-routes-use-selected-layout-segment/app/favicon.ico
Binary file not shown.
3 changes: 3 additions & 0 deletions
3
test/e2e/app-dir/parallel-routes-use-selected-layout-segment/app/foo/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return <div>/app/foo/page.tsx</div> | ||
} |
56 changes: 56 additions & 0 deletions
56
test/e2e/app-dir/parallel-routes-use-selected-layout-segment/app/layout.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
'use client' | ||
|
||
import { useSelectedLayoutSegment } from 'next/navigation' | ||
import Link from 'next/link' | ||
|
||
export default function RootLayout({ | ||
children, | ||
auth, | ||
nav, | ||
}: Readonly<{ | ||
children: React.ReactNode | ||
auth: React.ReactNode | ||
nav: React.ReactNode | ||
}>) { | ||
const authSegment = useSelectedLayoutSegment('auth') | ||
const navSegment = useSelectedLayoutSegment('nav') | ||
const routeSegment = useSelectedLayoutSegment() | ||
|
||
return ( | ||
<html lang="en"> | ||
<body> | ||
<section> | ||
<nav> | ||
<Link href="/">Main</Link> | ||
<Link href="/foo">Foo (regular page)</Link> | ||
<Link href="/login"> | ||
Login (/app/@auth/login) and (/app/@nav/login) | ||
</Link> | ||
<Link href="/reset">Reset (/app/@auth/reset)</Link> | ||
<Link href="/reset/withEmail"> | ||
Reset with Email (/app/@auth/reset/withEmail) | ||
</Link> | ||
<Link href="/reset/withMobile"> | ||
Reset with Mobile (/app/@auth/reset/withMobile) | ||
</Link> | ||
</nav> | ||
<div> | ||
navSegment (parallel route): <div id="navSegment">{navSegment}</div> | ||
</div> | ||
<div> | ||
authSegment (parallel route):{' '} | ||
<div id="authSegment">{authSegment}</div> | ||
</div> | ||
<div> | ||
routeSegment (app route):{' '} | ||
<div id="routeSegment">{routeSegment}</div> | ||
</div> | ||
|
||
<section id="navSlot">{nav}</section> | ||
<section id="authSlot">{auth}</section> | ||
<section id="children">{children}</section> | ||
</section> | ||
</body> | ||
</html> | ||
) | ||
} |
3 changes: 3 additions & 0 deletions
3
test/e2e/app-dir/parallel-routes-use-selected-layout-segment/app/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return <div>/app/page.tsx</div> | ||
} |
6 changes: 6 additions & 0 deletions
6
test/e2e/app-dir/parallel-routes-use-selected-layout-segment/next.config.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/** | ||
* @type {import('next').NextConfig} | ||
*/ | ||
const nextConfig = {} | ||
|
||
module.exports = nextConfig |
75 changes: 75 additions & 0 deletions
75
...el-routes-use-selected-layout-segment/parallel-routes-use-selected-layout-segment.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { createNextDescribe } from 'e2e-utils' | ||
import { check } from 'next-test-utils' | ||
|
||
createNextDescribe( | ||
'parallel-routes-use-selected-layout-segment', | ||
{ | ||
files: __dirname, | ||
}, | ||
({ next }) => { | ||
it('hard nav to router page and soft nav around other router pages', async () => { | ||
const browser = await next.browser('/') | ||
await check(() => browser.elementById('navSegment').text(), /^$/) | ||
await check(() => browser.elementById('authSegment').text(), /^$/) | ||
await check(() => browser.elementById('routeSegment').text(), /^$/) | ||
|
||
await browser.elementByCss('[href="/foo"]').click() | ||
await check(() => browser.elementById('navSegment').text(), /^$/) | ||
await check(() => browser.elementById('authSegment').text(), /^$/) | ||
await check(() => browser.elementById('routeSegment').text(), /foo/) | ||
}) | ||
|
||
it('hard nav to router page and soft nav to parallel routes', async () => { | ||
const browser = await next.browser('/') | ||
await check(() => browser.elementById('navSegment').text(), /^$/) | ||
await check(() => browser.elementById('authSegment').text(), /^$/) | ||
await check(() => browser.elementById('routeSegment').text(), /^$/) | ||
|
||
// soft nav to /login, since both @nav and @auth has /login defined, we expect both navSegment and authSegment to be 'login' | ||
await browser.elementByCss('[href="/login"]').click() | ||
await check(() => browser.elementById('navSegment').text(), /login/) | ||
await check(() => browser.elementById('authSegment').text(), /login/) | ||
await check(() => browser.elementById('routeSegment').text(), /^$/) | ||
|
||
// when navigating to /reset, the @auth slot will render the /reset page ('reset') while maintaining the currently active page for the @nav slot ('login') since /reset is only defined in @auth | ||
await browser.elementByCss('[href="/reset"]').click() | ||
await check(() => browser.elementById('navSegment').text(), /login/) | ||
await check(() => browser.elementById('authSegment').text(), /reset/) | ||
await check(() => browser.elementById('routeSegment').text(), /^$/) | ||
|
||
// when navigating to nested path /reset/withEmail, the @auth slot will render the nested /reset/withEmail page ('reset') while maintaining the currently active page for the @nav slot ('login') since /reset/withEmail is only defined in @auth | ||
await browser.elementByCss('[href="/reset/withEmail"]').click() | ||
await check(() => browser.elementById('navSegment').text(), /login/) | ||
await check(() => browser.elementById('authSegment').text(), /withEmail/) | ||
await check(() => browser.elementById('routeSegment').text(), /^$/) | ||
}) | ||
|
||
it('hard nav to router page and soft nav to parallel route and soft nav back to another router page', async () => { | ||
const browser = await next.browser('/') | ||
await check(() => browser.elementById('navSegment').text(), /^$/) | ||
await check(() => browser.elementById('authSegment').text(), /^$/) | ||
await check(() => browser.elementById('routeSegment').text(), /^$/) | ||
|
||
// when navigating to /reset, the @auth slot will render the /reset page ('reset') while maintaining the currently active page for the @nav slot ('null') since /reset is only defined in @auth | ||
await browser.elementByCss('[href="/reset"]').click() | ||
await check(() => browser.elementById('navSegment').text(), /^$/) | ||
await check(() => browser.elementById('authSegment').text(), /reset/) | ||
await check(() => browser.elementById('routeSegment').text(), /^$/) | ||
|
||
// when soft navigate to /foo, the @auth and @nav slot will maintain their the currently active states since they do not have /foo defined | ||
await browser.elementByCss('[href="/foo"]').click() | ||
await check(() => browser.elementById('navSegment').text(), /^$/) | ||
await check(() => browser.elementById('authSegment').text(), /reset/) | ||
await check(() => browser.elementById('routeSegment').text(), /foo/) | ||
}) | ||
|
||
it('hard nav to parallel route', async () => { | ||
const browser = await next.browser('/reset/withMobile') | ||
await check(() => browser.elementById('navSegment').text(), /^$/) | ||
await check(() => browser.elementById('authSegment').text(), /withMobile/) | ||
|
||
// the /app/default.tsx is rendered since /reset/withMobile is only defined in @auth | ||
await check(() => browser.elementById('routeSegment').text(), /^$/) | ||
}) | ||
} | ||
) |