diff --git a/packages/nuqs/src/tests/useQueryStates.test-d.ts b/packages/nuqs/src/tests/useQueryStates.test-d.ts index 46d351ba8..c6baa3209 100644 --- a/packages/nuqs/src/tests/useQueryStates.test-d.ts +++ b/packages/nuqs/src/tests/useQueryStates.test-d.ts @@ -25,7 +25,7 @@ describe('types/useQueryStates', () => { setState({ a: null }) // Clear an individual key setState(null) // Clear all managed keys setState(() => ({ a: null })) - // setState(() => null) // todo: Enable this test in a separate PR + setState(() => null) }) it('allows setting to undefined to leave keys as-is', () => { const [, setState] = useQueryStates(parsers) @@ -53,7 +53,7 @@ describe('types/useQueryStates', () => { return {} }) setState(() => ({ a: null, b: null })) // Still allowed to clear it with null (state retuns to default) - // setState(() => null) // todo: Enable this test in a separate PR + setState(() => null) }) it('supports inline custom parsers', () => { const [state] = useQueryStates({ diff --git a/packages/nuqs/src/useQueryStates.test.ts b/packages/nuqs/src/useQueryStates.test.ts index a1b0958ef..18ee3847f 100644 --- a/packages/nuqs/src/useQueryStates.test.ts +++ b/packages/nuqs/src/useQueryStates.test.ts @@ -7,6 +7,89 @@ import { import { parseAsArrayOf, parseAsJson, parseAsString } from './parsers' import { useQueryStates } from './useQueryStates' +describe('useQueryStates', () => { + it('allows setting a single value', async () => { + const onUrlUpdate = vi.fn() + const useTestHook = () => + useQueryStates({ + a: parseAsString, + b: parseAsString + }) + const { result } = renderHook(useTestHook, { + wrapper: withNuqsTestingAdapter({ + onUrlUpdate + }) + }) + expect(result.current[0].a).toBeNull() + expect(result.current[0].b).toBeNull() + await act(() => result.current[1]({ a: 'pass' })) + expect(result.current[0].a).toEqual('pass') + expect(result.current[0].b).toBeNull() + expect(onUrlUpdate).toHaveBeenCalledOnce() + expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('?a=pass') + }) + + it('allows clearing a single key by setting it to null', async () => { + const onUrlUpdate = vi.fn() + const useTestHook = () => + useQueryStates({ + a: parseAsString, + b: parseAsString + }) + const { result } = renderHook(useTestHook, { + wrapper: withNuqsTestingAdapter({ + searchParams: '?a=init&b=init', + onUrlUpdate + }) + }) + expect(result.current[0].a).toEqual('init') + expect(result.current[0].b).toEqual('init') + await act(() => result.current[1]({ a: null })) + expect(result.current[0].a).toBeNull() + expect(result.current[0].b).toEqual('init') + expect(onUrlUpdate).toHaveBeenCalledOnce() + expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('?b=init') + }) + it('allows clearing managed keys by passing null', async () => { + const onUrlUpdate = vi.fn() + const useTestHook = () => + useQueryStates({ + a: parseAsString, + b: parseAsString + }) + const { result } = renderHook(useTestHook, { + wrapper: withNuqsTestingAdapter({ + searchParams: '?a=init&b=init', + onUrlUpdate + }) + }) + await act(() => result.current[1](null)) + expect(result.current[0].a).toBeNull() + expect(result.current[0].b).toBeNull() + expect(onUrlUpdate).toHaveBeenCalledOnce() + expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('') + }) + it('allows clearing managed keys by passing a function that returns null', async () => { + const onUrlUpdate = vi.fn() + const useTestHook = () => + useQueryStates({ + a: parseAsString, + b: parseAsString + }) + const { result } = renderHook(useTestHook, { + wrapper: withNuqsTestingAdapter({ + searchParams: '?a=init&b=init', + onUrlUpdate + }) + }) + await act(() => result.current[1](() => null)) + expect(result.current[0].a).toBeNull() + expect(result.current[0].b).toBeNull() + expect(onUrlUpdate).toHaveBeenCalledOnce() + expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('') + }) +}) + describe('useQueryStates: referential equality', () => { const defaults = { str: 'foo', diff --git a/packages/nuqs/src/useQueryStates.ts b/packages/nuqs/src/useQueryStates.ts index 330958aed..45e4cfa47 100644 --- a/packages/nuqs/src/useQueryStates.ts +++ b/packages/nuqs/src/useQueryStates.ts @@ -37,7 +37,7 @@ type NullableValues = Nullable> type UpdaterFn = ( old: Values -) => Partial>> +) => Partial>> | null export type SetValues = ( values: Partial>> | UpdaterFn | null, @@ -191,14 +191,15 @@ export function useQueryStates( const update = useCallback>( (stateUpdater, callOptions = {}) => { + const nullMap = Object.fromEntries( + Object.keys(keyMap).map(key => [key, null]) + ) as Nullable const newState: Partial> = typeof stateUpdater === 'function' - ? stateUpdater(applyDefaultValues(stateRef.current, defaultValues)) - : stateUpdater === null - ? (Object.fromEntries( - Object.keys(keyMap).map(key => [key, null]) - ) as Nullable) - : stateUpdater + ? (stateUpdater( + applyDefaultValues(stateRef.current, defaultValues) + ) ?? nullMap) + : (stateUpdater ?? nullMap) debug('[nuq+ `%s`] setState: %O', stateKeys, newState) for (let [stateKey, value] of Object.entries(newState)) { const parser = keyMap[stateKey]