Skip to content

Commit 4c335a5

Browse files
authored
feat: Allow clearing all search params managed by useQueryStates (#622)
* feat: Allow clearing all search params managed by useQueryStates By passing `null` instead of an object, every key managed by that`useQueryStates` hook will be cleared from the URL.
1 parent 099ceb3 commit 4c335a5

File tree

6 files changed

+55
-12
lines changed

6 files changed

+55
-12
lines changed

packages/docs/content/docs/batching.mdx

+13
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,16 @@ setCoordinates({
9898
```
9999

100100
The order of precedence is: call-level options > parser options > global options.
101+
102+
<Callout title="Tip">
103+
You can clear all keys managed by a `useQueryStates` hook by passing
104+
`null` to the state updater function.
105+
106+
```ts
107+
const clearAll = () => setCoordinates(null)
108+
```
109+
110+
This will clear `lat` & `lng`, and leave other search params untouched.
111+
112+
</Callout>
113+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/// <reference types="cypress" />
2+
3+
it('useQueryStates clear all', () => {
4+
cy.visit('/app/useQueryStates-clear-all?a=foo&b=bar')
5+
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
6+
cy.get('button').click()
7+
cy.location('search').should('eq', '')
8+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client'
2+
3+
import { parseAsString, useQueryStates } from 'nuqs'
4+
import { Suspense } from 'react'
5+
6+
export default function Page() {
7+
return (
8+
<Suspense>
9+
<Client />
10+
</Suspense>
11+
)
12+
}
13+
14+
function Client() {
15+
const [, setValues] = useQueryStates({
16+
a: parseAsString.withDefault(''),
17+
b: parseAsString.withDefault('')
18+
})
19+
return (
20+
<>
21+
<button onClick={() => setValues(null)}>Clear</button>
22+
</>
23+
)
24+
}

packages/nuqs/src/tests/compat/useQueryStates.test-d.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expectError, expectNotAssignable, expectType } from 'tsd'
1+
import { expectNotAssignable, expectType } from 'tsd'
22
import { queryTypes, useQueryStates } from '../../../dist'
33

44
{
@@ -50,10 +50,7 @@ import { queryTypes, useQueryStates } from '../../../dist'
5050
hasDefault: null,
5151
doesNot: null
5252
}))
53-
// but not at root level
54-
expectError(() => {
55-
setStates(null)
56-
})
53+
setStates(null)
5754
}
5855

5956
// Custom parsers

packages/nuqs/src/tests/useQueryStates.test-d.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expectError, expectNotAssignable, expectType } from 'tsd'
1+
import { expectNotAssignable, expectType } from 'tsd'
22
import {
33
parseAsBoolean,
44
parseAsFloat,
@@ -57,10 +57,7 @@ import {
5757
hasDefault: null,
5858
doesNot: null
5959
}))
60-
// but not at root level
61-
expectError(() => {
62-
setStates(null)
63-
})
60+
setStates(null)
6461
}
6562

6663
// Custom parsers

packages/nuqs/src/useQueryStates.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ type UpdaterFn<T extends UseQueryStatesKeysMap> = (
4040
) => Partial<Nullable<Values<T>>>
4141

4242
export type SetValues<T extends UseQueryStatesKeysMap> = (
43-
values: Partial<Nullable<Values<T>>> | UpdaterFn<T>,
43+
values: Partial<Nullable<Values<T>>> | UpdaterFn<T> | null,
4444
options?: Options
4545
) => Promise<URLSearchParams>
4646

@@ -140,7 +140,11 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
140140
const newState: Partial<Nullable<KeyMap>> =
141141
typeof stateUpdater === 'function'
142142
? stateUpdater(stateRef.current)
143-
: stateUpdater
143+
: stateUpdater === null
144+
? (Object.fromEntries(
145+
Object.keys(keyMap).map(key => [key, null])
146+
) as Nullable<KeyMap>)
147+
: stateUpdater
144148
debug('[nuq+ `%s`] setState: %O', keys, newState)
145149
for (let [key, value] of Object.entries(newState)) {
146150
const parser = keyMap[key]

0 commit comments

Comments
 (0)