Skip to content

Commit 23c57cb

Browse files
authored
fix: Dynamic keys for useQueryStates (47ng#858)
* fix: Dynamic keys for useQueryStates * test: Add dynamic keys test * chore: Always test with two dynamic keys
1 parent 2ac1e44 commit 23c57cb

File tree

2 files changed

+91
-11
lines changed

2 files changed

+91
-11
lines changed

packages/nuqs/src/useQueryStates.test.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import {
44
withNuqsTestingAdapter,
55
type OnUrlUpdateFunction
66
} from './adapters/testing'
7-
import { parseAsArrayOf, parseAsJson, parseAsString } from './parsers'
7+
import {
8+
parseAsArrayOf,
9+
parseAsInteger,
10+
parseAsJson,
11+
parseAsString
12+
} from './parsers'
813
import { useQueryStates } from './useQueryStates'
914

1015
describe('useQueryStates', () => {
@@ -316,3 +321,64 @@ describe('useQueryStates: clearOnDefault', () => {
316321
expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('')
317322
})
318323
})
324+
325+
describe('useQueryStates: dynamic keys', () => {
326+
it('supports dynamic keys', () => {
327+
const useTestHook = (keys: [string, string] = ['a', 'b']) =>
328+
useQueryStates({
329+
[keys[0]]: parseAsInteger,
330+
[keys[1]]: parseAsInteger
331+
})
332+
const { result, rerender } = renderHook(useTestHook, {
333+
wrapper: withNuqsTestingAdapter({
334+
searchParams: '?a=1&b=2&c=3&d=4'
335+
})
336+
})
337+
expect(result.current[0].a).toEqual(1)
338+
expect(result.current[0].b).toEqual(2)
339+
expect(result.current[0].c).toBeUndefined()
340+
expect(result.current[0].d).toBeUndefined()
341+
rerender(['c', 'd'])
342+
expect(result.current[0].a).toBeUndefined()
343+
expect(result.current[0].b).toBeUndefined()
344+
expect(result.current[0].c).toEqual(3)
345+
expect(result.current[0].d).toEqual(4)
346+
})
347+
348+
it('supports dynamic keys with remapping', () => {
349+
const useTestHook = (keys: [string, string] = ['a', 'b']) =>
350+
useQueryStates(
351+
{
352+
[keys[0]]: parseAsInteger,
353+
[keys[1]]: parseAsInteger
354+
},
355+
{
356+
urlKeys: {
357+
a: 'x',
358+
b: 'y',
359+
c: 'z'
360+
}
361+
}
362+
)
363+
const { result, rerender } = renderHook(useTestHook, {
364+
wrapper: withNuqsTestingAdapter({
365+
searchParams: '?x=1&y=2&z=3'
366+
})
367+
})
368+
expect(result.current[0].a).toEqual(1)
369+
expect(result.current[0].b).toEqual(2)
370+
expect(result.current[0].c).toBeUndefined()
371+
expect(result.current[0].d).toBeUndefined()
372+
expect(result.current[0].x).toBeUndefined()
373+
expect(result.current[0].y).toBeUndefined()
374+
expect(result.current[0].z).toBeUndefined()
375+
rerender(['c', 'd'])
376+
expect(result.current[0].a).toBeUndefined()
377+
expect(result.current[0].b).toBeUndefined()
378+
expect(result.current[0].c).toEqual(3)
379+
expect(result.current[0].d).toBeNull()
380+
expect(result.current[0].x).toBeUndefined()
381+
expect(result.current[0].y).toBeUndefined()
382+
expect(result.current[0].z).toBeUndefined()
383+
})
384+
})

packages/nuqs/src/useQueryStates.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,11 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
8181
Object.fromEntries(
8282
Object.keys(keyMap).map(key => [key, urlKeys[key] ?? key])
8383
),
84-
[stateKeys, urlKeys]
84+
[stateKeys, JSON.stringify(urlKeys)]
8585
)
8686
const adapter = useAdapter()
8787
const initialSearchParams = adapter.searchParams
8888
const queryRef = useRef<Record<string, string | null>>({})
89-
// Initialise the queryRef with the initial values
90-
if (Object.keys(queryRef.current).length !== Object.keys(keyMap).length) {
91-
queryRef.current = Object.fromEntries(
92-
Object.values(resolvedUrlKeys).map(urlKey => [
93-
urlKey,
94-
initialSearchParams?.get(urlKey) ?? null
95-
])
96-
)
97-
}
9889
const defaultValues = useMemo(
9990
() =>
10091
Object.fromEntries(
@@ -119,6 +110,29 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
119110
internalState,
120111
initialSearchParams
121112
)
113+
// Initialise the refs with the initial values
114+
if (
115+
Object.keys(queryRef.current).join('&') !==
116+
Object.values(resolvedUrlKeys).join('&')
117+
) {
118+
const { state, hasChanged } = parseMap(
119+
keyMap,
120+
urlKeys,
121+
initialSearchParams,
122+
queryRef.current,
123+
stateRef.current
124+
)
125+
if (hasChanged) {
126+
stateRef.current = state
127+
setInternalState(state)
128+
}
129+
queryRef.current = Object.fromEntries(
130+
Object.values(resolvedUrlKeys).map(urlKey => [
131+
urlKey,
132+
initialSearchParams?.get(urlKey) ?? null
133+
])
134+
)
135+
}
122136

123137
useEffect(() => {
124138
const { state, hasChanged } = parseMap(

0 commit comments

Comments
 (0)