Skip to content

Commit 3264d91

Browse files
authored
memoize useParams (#56771)
`useParams` is not referentially equal between renders which can lead to unexpected behavior when used as a dep. This memoizes the response from `useParams` similar to `useSearchParams`. [slack x-ref](https://vercel.slack.com/archives/C04DUD7EB1B/p1697145987740229)
1 parent e0cd065 commit 3264d91

File tree

4 files changed

+115
-6
lines changed

4 files changed

+115
-6
lines changed

packages/next/src/client/components/navigation.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,15 @@ export function useParams<T extends Params = Params>(): T {
170170
const globalLayoutRouter = useContext(GlobalLayoutRouterContext)
171171
const pathParams = useContext(PathParamsContext)
172172

173-
// When it's under app router
174-
if (globalLayoutRouter) {
175-
return getSelectedParams(globalLayoutRouter.tree) as T
176-
}
173+
return useMemo(() => {
174+
// When it's under app router
175+
if (globalLayoutRouter?.tree) {
176+
return getSelectedParams(globalLayoutRouter.tree) as T
177+
}
177178

178-
// When it's under client side pages router
179-
return pathParams as T
179+
// When it's under client side pages router
180+
return pathParams as T
181+
}, [globalLayoutRouter?.tree, pathParams])
180182
}
181183

182184
// TODO-APP: handle parallel routes
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use client'
2+
import { useParams, useRouter } from 'next/navigation'
3+
import { useState } from 'react'
4+
import { useEffect } from 'react'
5+
6+
export default function Page() {
7+
const params = useParams()
8+
const router = useRouter()
9+
const [count, setCount] = useState(0)
10+
useEffect(() => {
11+
console.log('params changed')
12+
}, [params])
13+
return (
14+
<div>
15+
<button
16+
id="rerender-button"
17+
onClick={() => setCount((count) => count + 1)}
18+
>
19+
Re-Render {count}
20+
</button>
21+
22+
<button
23+
id="change-params-button"
24+
onClick={() => router.push('/search-params/bar')}
25+
>
26+
Change Params
27+
</button>
28+
</div>
29+
)
30+
}

test/e2e/app-dir/navigation/navigation.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,54 @@ createNextDescribe(
5454
: JSON.stringify(requests)
5555
}, 'success')
5656
})
57+
58+
describe('useParams identity between renders', () => {
59+
async function runTests(page: string) {
60+
const browser = await next.browser(page)
61+
62+
await check(
63+
async () => JSON.stringify(await browser.log()),
64+
/params changed/
65+
)
66+
67+
let outputIndex = (await browser.log()).length
68+
69+
await browser.elementById('rerender-button').click()
70+
await browser.elementById('rerender-button').click()
71+
await browser.elementById('rerender-button').click()
72+
73+
await check(async () => {
74+
return browser.elementById('rerender-button').text()
75+
}, 'Re-Render 3')
76+
77+
await check(async () => {
78+
const logs = await browser.log()
79+
return JSON.stringify(logs.slice(outputIndex)).includes(
80+
'params changed'
81+
)
82+
? 'fail'
83+
: 'success'
84+
}, 'success')
85+
86+
outputIndex = (await browser.log()).length
87+
88+
await browser.elementById('change-params-button').click()
89+
90+
await check(
91+
async () =>
92+
JSON.stringify((await browser.log()).slice(outputIndex)),
93+
/params changed/
94+
)
95+
}
96+
97+
it('should be stable in app', async () => {
98+
await runTests('/search-params/foo')
99+
})
100+
101+
it('should be stable in pages', async () => {
102+
await runTests('/search-params-pages/foo')
103+
})
104+
})
57105
})
58106

59107
describe('hash', () => {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useParams, useRouter } from 'next/navigation'
2+
import { useState } from 'react'
3+
import { useEffect } from 'react'
4+
5+
export default function Page() {
6+
const params = useParams()
7+
const router = useRouter()
8+
const [count, setCount] = useState(0)
9+
useEffect(() => {
10+
console.log('params changed')
11+
}, [params])
12+
return (
13+
<div>
14+
<button
15+
id="rerender-button"
16+
onClick={() => setCount((count) => count + 1)}
17+
>
18+
Re-Render {count}
19+
</button>
20+
21+
<button
22+
id="change-params-button"
23+
onClick={() => router.push('/search-params-pages/bar')}
24+
>
25+
Change Params
26+
</button>
27+
</div>
28+
)
29+
}

0 commit comments

Comments
 (0)