Skip to content

Commit

Permalink
fix navigation issue when dynamic param casing changes (#61726)
Browse files Browse the repository at this point in the history
### What
When navigating to a page with dynamic params using a certain casing,
and then following a link to another page using _different_ casing for
the same param, the router would get stuck in an infinite suspense
cycle.

### Why
On the client we normalize cache keys by lowercasing the values for
dynamic segments. However the RSC data for each segment wouldn't have
this same casing logic applied. This is causing the router to not
recognize that there is already RSC data available for that segment,
resulting in an infinite suspense cycle.

### How
The `toLowerCase()` logic shouldn't be needed here. Technically we could
leave this in place and update `matchSegment` to also apply the
lowercase logic, but currently there are too many utility functions that
parse segments to comfortably make that change. I confirmed that the bug
related to why we lowercased these router cache keys is no longer
present after making this change.

Fixes #61722
Closes NEXT-2377
  • Loading branch information
ztanner authored Feb 6, 2024
1 parent df74a02 commit a19b3bc
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function createRouterCacheKey(
// if the segment is an array, it means it's a dynamic segment
// for example, ['lang', 'en', 'd']. We need to convert it to a string to store it as a cache node key.
if (Array.isArray(segment)) {
return `${segment[0]}|${segment[1]}|${segment[2]}`.toLowerCase()
return `${segment[0]}|${segment[1]}|${segment[2]}`
}

// Page segments might have search parameters, ie __PAGE__?foo=bar
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>[paramB] page</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>noParam page</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Link from 'next/link'
import React from 'react'

export default function Page() {
return (
<>
<Link href="/dynamic-param-casing-change/paramA/paramB">
/paramA/paramB
</Link>

<div>
<Link href="/dynamic-param-casing-change/paramA/noParam">
/paramA/noParam
</Link>
</div>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Link from 'next/link'
import React from 'react'

export default function Page() {
return <Link href="/dynamic-param-casing-change/ParamA">/ParamA</Link>
}
34 changes: 34 additions & 0 deletions test/e2e/app-dir/navigation/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,5 +817,39 @@ createNextDescribe(
})
})
})

describe('navigating to dynamic params & changing the casing', () => {
it('should load the page correctly', async () => {
const browser = await next.browser('/dynamic-param-casing-change')

// note the casing here capitalizes `ParamA`
await browser
.elementByCss("[href='/dynamic-param-casing-change/ParamA']")
.click()

// note the `paramA` casing has now changed
await browser
.elementByCss("[href='/dynamic-param-casing-change/paramA/noParam']")
.click()

await retry(async () => {
expect(await browser.elementByCss('body').text()).toContain(
'noParam page'
)
})

await browser.back()

await browser
.elementByCss("[href='/dynamic-param-casing-change/paramA/paramB']")
.click()

await retry(async () => {
expect(await browser.elementByCss('body').text()).toContain(
'[paramB] page'
)
})
})
})
}
)

0 comments on commit a19b3bc

Please sign in to comment.