Skip to content

Commit a504e0e

Browse files
authored
fix: Update useQueryStates state cache when syncing useSearchParams (#776)
This fixes an issue where `<Link>` navigation (or external updates of the URL) kept an internal stale state ref and reused it when updating other search params.
1 parent 672aa53 commit a504e0e

File tree

3 files changed

+64
-5
lines changed

3 files changed

+64
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference types="cypress" />
2+
3+
describe('repro-774', () => {
4+
it('updates internal state on navigation', () => {
5+
cy.visit('/app/repro-774')
6+
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
7+
cy.get('#trigger-a').click()
8+
cy.get('#value-a').should('have.text', 'a')
9+
cy.get('#value-b').should('be.empty')
10+
cy.get('#link').click()
11+
cy.get('#value-a').should('be.empty')
12+
cy.get('#value-b').should('be.empty')
13+
cy.get('#trigger-b').click()
14+
cy.get('#value-a').should('be.empty')
15+
cy.get('#value-b').should('have.text', 'b')
16+
})
17+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client'
2+
3+
import Link from 'next/link'
4+
import { parseAsString, useQueryStates } from 'nuqs'
5+
import { Suspense } from 'react'
6+
7+
export default function Home() {
8+
return (
9+
<>
10+
<nav>
11+
<Link id="link" href="/app/repro-774">
12+
Reset
13+
</Link>
14+
</nav>
15+
<Suspense>
16+
<Client />
17+
</Suspense>
18+
</>
19+
)
20+
}
21+
22+
const searchParams = {
23+
a: parseAsString.withDefault(''),
24+
b: parseAsString.withDefault('')
25+
}
26+
27+
function Client() {
28+
const [{ a, b }, setSearchParams] = useQueryStates(searchParams)
29+
return (
30+
<>
31+
<button onClick={() => setSearchParams({ a: 'a' })} id="trigger-a">
32+
Set A
33+
</button>
34+
<button onClick={() => setSearchParams({ b: 'b' })} id="trigger-b">
35+
Set B
36+
</button>
37+
<span id="value-a">{a}</span>
38+
<span id="value-b">{b}</span>
39+
</>
40+
)
41+
}

packages/nuqs/src/useQueryStates.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
133133
queryRef.current,
134134
stateRef.current
135135
)
136+
stateRef.current = state
136137
setInternalState(state)
137138
}, [
138139
Object.values(resolvedUrlKeys)
@@ -274,7 +275,7 @@ function parseMap<KeyMap extends UseQueryStatesKeysMap>(
274275
cachedQuery?: Record<string, string | null>,
275276
cachedState?: NullableValues<KeyMap>
276277
): NullableValues<KeyMap> {
277-
return Object.keys(keyMap).reduce((obj, stateKey) => {
278+
return Object.keys(keyMap).reduce((out, stateKey) => {
278279
const urlKey = urlKeys?.[stateKey] ?? stateKey
279280
const { parse } = keyMap[stateKey]!
280281
const queuedQuery = getQueuedValue(urlKey)
@@ -283,15 +284,15 @@ function parseMap<KeyMap extends UseQueryStatesKeysMap>(
283284
? (searchParams?.get(urlKey) ?? null)
284285
: queuedQuery
285286
if (cachedQuery && cachedState && cachedQuery[urlKey] === query) {
286-
obj[stateKey as keyof KeyMap] = cachedState[stateKey] ?? null
287-
return obj
287+
out[stateKey as keyof KeyMap] = cachedState[stateKey] ?? null
288+
return out
288289
}
289290
const value = query === null ? null : safeParse(parse, query, stateKey)
290-
obj[stateKey as keyof KeyMap] = value ?? null
291+
out[stateKey as keyof KeyMap] = value ?? null
291292
if (cachedQuery) {
292293
cachedQuery[urlKey] = query
293294
}
294-
return obj
295+
return out
295296
}, {} as NullableValues<KeyMap>)
296297
}
297298

0 commit comments

Comments
 (0)