Skip to content

Commit

Permalink
fix: Allow useQueryStates' state updater function to return null (#871)
Browse files Browse the repository at this point in the history
  • Loading branch information
franky47 authored Jan 23, 2025
1 parent ba5ce36 commit 894e141
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 9 deletions.
4 changes: 2 additions & 2 deletions packages/nuqs/src/tests/useQueryStates.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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({
Expand Down
83 changes: 83 additions & 0 deletions packages/nuqs/src/useQueryStates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OnUrlUpdateFunction>()
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<OnUrlUpdateFunction>()
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<OnUrlUpdateFunction>()
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<OnUrlUpdateFunction>()
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',
Expand Down
15 changes: 8 additions & 7 deletions packages/nuqs/src/useQueryStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type NullableValues<T extends UseQueryStatesKeysMap> = Nullable<Values<T>>

type UpdaterFn<T extends UseQueryStatesKeysMap> = (
old: Values<T>
) => Partial<Nullable<Values<T>>>
) => Partial<Nullable<Values<T>>> | null

export type SetValues<T extends UseQueryStatesKeysMap> = (
values: Partial<Nullable<Values<T>>> | UpdaterFn<T> | null,
Expand Down Expand Up @@ -191,14 +191,15 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(

const update = useCallback<SetValues<KeyMap>>(
(stateUpdater, callOptions = {}) => {
const nullMap = Object.fromEntries(
Object.keys(keyMap).map(key => [key, null])
) as Nullable<KeyMap>
const newState: Partial<Nullable<KeyMap>> =
typeof stateUpdater === 'function'
? stateUpdater(applyDefaultValues(stateRef.current, defaultValues))
: stateUpdater === null
? (Object.fromEntries(
Object.keys(keyMap).map(key => [key, null])
) as Nullable<KeyMap>)
: 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]
Expand Down

0 comments on commit 894e141

Please sign in to comment.