diff --git a/packages/nuqs/src/useQueryStates.test.ts b/packages/nuqs/src/useQueryStates.test.ts index 18ee3847f..2542d7a5d 100644 --- a/packages/nuqs/src/useQueryStates.test.ts +++ b/packages/nuqs/src/useQueryStates.test.ts @@ -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', () => { @@ -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() + }) +}) diff --git a/packages/nuqs/src/useQueryStates.ts b/packages/nuqs/src/useQueryStates.ts index 45e4cfa47..2a56be2ab 100644 --- a/packages/nuqs/src/useQueryStates.ts +++ b/packages/nuqs/src/useQueryStates.ts @@ -81,20 +81,11 @@ export function useQueryStates( 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>({}) - // 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( @@ -119,6 +110,29 @@ export function useQueryStates( 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(