Skip to content

Commit

Permalink
fix: Dynamic keys for useQueryStates (#858)
Browse files Browse the repository at this point in the history
* fix: Dynamic keys for useQueryStates

* test: Add dynamic keys test

* chore: Always test with two dynamic keys
  • Loading branch information
franky47 authored Jan 28, 2025
1 parent 2ac1e44 commit 23c57cb
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 11 deletions.
68 changes: 67 additions & 1 deletion packages/nuqs/src/useQueryStates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import {
withNuqsTestingAdapter,
type OnUrlUpdateFunction
} from './adapters/testing'
import { parseAsArrayOf, parseAsJson, parseAsString } from './parsers'
import {
parseAsArrayOf,
parseAsInteger,
parseAsJson,
parseAsString
} from './parsers'
import { useQueryStates } from './useQueryStates'

describe('useQueryStates', () => {
Expand Down Expand Up @@ -316,3 +321,64 @@ describe('useQueryStates: clearOnDefault', () => {
expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('')
})
})

describe('useQueryStates: dynamic keys', () => {
it('supports dynamic keys', () => {
const useTestHook = (keys: [string, string] = ['a', 'b']) =>
useQueryStates({
[keys[0]]: parseAsInteger,
[keys[1]]: parseAsInteger
})
const { result, rerender } = renderHook(useTestHook, {
wrapper: withNuqsTestingAdapter({
searchParams: '?a=1&b=2&c=3&d=4'
})
})
expect(result.current[0].a).toEqual(1)
expect(result.current[0].b).toEqual(2)
expect(result.current[0].c).toBeUndefined()
expect(result.current[0].d).toBeUndefined()
rerender(['c', 'd'])
expect(result.current[0].a).toBeUndefined()
expect(result.current[0].b).toBeUndefined()
expect(result.current[0].c).toEqual(3)
expect(result.current[0].d).toEqual(4)
})

it('supports dynamic keys with remapping', () => {
const useTestHook = (keys: [string, string] = ['a', 'b']) =>
useQueryStates(
{
[keys[0]]: parseAsInteger,
[keys[1]]: parseAsInteger
},
{
urlKeys: {
a: 'x',
b: 'y',
c: 'z'
}
}
)
const { result, rerender } = renderHook(useTestHook, {
wrapper: withNuqsTestingAdapter({
searchParams: '?x=1&y=2&z=3'
})
})
expect(result.current[0].a).toEqual(1)
expect(result.current[0].b).toEqual(2)
expect(result.current[0].c).toBeUndefined()
expect(result.current[0].d).toBeUndefined()
expect(result.current[0].x).toBeUndefined()
expect(result.current[0].y).toBeUndefined()
expect(result.current[0].z).toBeUndefined()
rerender(['c', 'd'])
expect(result.current[0].a).toBeUndefined()
expect(result.current[0].b).toBeUndefined()
expect(result.current[0].c).toEqual(3)
expect(result.current[0].d).toBeNull()
expect(result.current[0].x).toBeUndefined()
expect(result.current[0].y).toBeUndefined()
expect(result.current[0].z).toBeUndefined()
})
})
34 changes: 24 additions & 10 deletions packages/nuqs/src/useQueryStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,11 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
Object.fromEntries(
Object.keys(keyMap).map(key => [key, urlKeys[key] ?? key])
),
[stateKeys, urlKeys]
[stateKeys, JSON.stringify(urlKeys)]
)
const adapter = useAdapter()
const initialSearchParams = adapter.searchParams
const queryRef = useRef<Record<string, string | null>>({})
// Initialise the queryRef with the initial values
if (Object.keys(queryRef.current).length !== Object.keys(keyMap).length) {
queryRef.current = Object.fromEntries(
Object.values(resolvedUrlKeys).map(urlKey => [
urlKey,
initialSearchParams?.get(urlKey) ?? null
])
)
}
const defaultValues = useMemo(
() =>
Object.fromEntries(
Expand All @@ -119,6 +110,29 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
internalState,
initialSearchParams
)
// Initialise the refs with the initial values
if (
Object.keys(queryRef.current).join('&') !==
Object.values(resolvedUrlKeys).join('&')
) {
const { state, hasChanged } = parseMap(
keyMap,
urlKeys,
initialSearchParams,
queryRef.current,
stateRef.current
)
if (hasChanged) {
stateRef.current = state
setInternalState(state)
}
queryRef.current = Object.fromEntries(
Object.values(resolvedUrlKeys).map(urlKey => [
urlKey,
initialSearchParams?.get(urlKey) ?? null
])
)
}

useEffect(() => {
const { state, hasChanged } = parseMap(
Expand Down

0 comments on commit 23c57cb

Please sign in to comment.