Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e8af136
feat: multi-parsers
TkDodo Sep 13, 2025
db3c11e
chore: increase size limit
TkDodo Sep 13, 2025
2fd2a23
fix: read is too broad for useQueryStates
TkDodo Sep 13, 2025
44820f5
ref: remove read abstraction
TkDodo Sep 13, 2025
d8e26a9
ref: bump size limit some more
TkDodo Sep 13, 2025
bc9786d
test: add some tests for parseAsNativeArray
TkDodo Sep 13, 2025
d622cea
test: native-array e2e tests
TkDodo Sep 13, 2025
6c95153
fix: return null from parsing if everything is unparsable
TkDodo Sep 13, 2025
864e0cd
test: add withDefault([]) to parseAsNativeArrayOf in
TkDodo Sep 14, 2025
c8da7d1
ref: type guard
TkDodo Sep 14, 2025
effb49c
ref: use object.entries over object.keys with an indexed access and a…
TkDodo Sep 14, 2025
ff10737
ref: rename isEmpty to avoid ambiguity
TkDodo Sep 14, 2025
e0021b1
fix: compareQuery for iterables
TkDodo Sep 14, 2025
cc89e20
fix: explicitly set searchParam to empty string if we get an empty it…
TkDodo Sep 14, 2025
27a354a
test: fix wrong parser assumptions
TkDodo Sep 14, 2025
c38d780
fix: switch to comparing all values in key-isolation
TkDodo Sep 14, 2025
c1f2ef9
Merge branch 'next' into feature/multi-parsers
TkDodo Sep 14, 2025
6f01b4d
fix: defensive check for standard schema
TkDodo Sep 14, 2025
ef68422
Update packages/nuqs/src/lib/search-params.ts
TkDodo Sep 15, 2025
8e4ebf4
feat: add .withDefault([]) to parseAsNativeArrayOf
TkDodo Sep 19, 2025
0be1405
ref: rename defaultValue to fallbackValue
TkDodo Sep 19, 2025
0411386
chore: leave a comment about the special empty value set
TkDodo Sep 19, 2025
c0b9677
Merge branch 'next' into feature/multi-parsers
TkDodo Sep 19, 2025
f036840
fix: types for parseAsNativeArray
TkDodo Sep 19, 2025
f5bf1b8
test: compare tests
TkDodo Sep 24, 2025
6b14b75
fix: keep backwards compatibility for Parser / ParserBuilder
TkDodo Sep 24, 2025
8fcd61b
doc: parseAsNativeArrayOf
TkDodo Sep 24, 2025
976aeb9
ref: move away from Iterables towards Arrays
TkDodo Sep 24, 2025
79f4249
fix: compare tests
TkDodo Sep 24, 2025
660af9d
doc: createMultiParser demo
TkDodo Sep 25, 2025
0622d04
Update packages/docs/content/docs/parsers/built-in.mdx
TkDodo Sep 25, 2025
4bf2ea2
doc: align clear button
TkDodo Sep 25, 2025
7013a5a
fix: NaN
TkDodo Sep 25, 2025
9f42bfc
doc: show values
TkDodo Sep 25, 2025
b0a2277
doc: simplify example
TkDodo Sep 25, 2025
fb185ce
doc: styles & wording for native arrays section & demo
franky47 Sep 26, 2025
11d8951
doc: add block about equality function
franky47 Sep 26, 2025
c9060e6
doc: custom multiparsers wording & demo
franky47 Sep 26, 2025
dfd874f
ref: rename QueryParam type to Query
franky47 Sep 26, 2025
551131b
ref: import type
franky47 Sep 26, 2025
f149875
feat: handle multi parsers in bijectivity testing helpers
franky47 Sep 26, 2025
875beaf
doc: mark MultiParser's parseServerSide as deprecated
franky47 Sep 26, 2025
805ed6a
chore: allow a bit more headroom on the server bundle
franky47 Sep 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/nuqs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@
{
"name": "Server",
"path": "dist/server.js",
"limit": "3 kB"
"limit": "3.1 kB"
}
]
}
4 changes: 4 additions & 0 deletions packages/nuqs/src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const exports = `
{
".": {
"createLoader": "function",
"createMultiParser": "function",
"createParser": "function",
"createSerializer": "function",
"createStandardSchemaV1": "function",
Expand All @@ -22,6 +23,7 @@ const exports = `
"parseAsIsoDate": "object",
"parseAsIsoDateTime": "object",
"parseAsJson": "function",
"parseAsNativeArrayOf": "function",
"parseAsNumberLiteral": "function",
"parseAsString": "object",
"parseAsStringEnum": "function",
Expand Down Expand Up @@ -73,6 +75,7 @@ const exports = `
},
"./server": {
"createLoader": "function",
"createMultiParser": "function",
"createParser": "function",
"createSearchParamsCache": "function",
"createSerializer": "function",
Expand All @@ -88,6 +91,7 @@ const exports = `
"parseAsIsoDate": "object",
"parseAsIsoDateTime": "object",
"parseAsJson": "function",
"parseAsNativeArrayOf": "function",
"parseAsNumberLiteral": "function",
"parseAsString": "object",
"parseAsStringEnum": "function",
Expand Down
6 changes: 4 additions & 2 deletions packages/nuqs/src/lib/queues/debounce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ export class DebounceController {
this.throttleQueue = throttleQueue
}

useQueuedQueries(keys: string[]): Record<string, string | null | undefined> {
useQueuedQueries(
keys: string[]
): Record<string, Iterable<string> | null | undefined> {
return useSyncExternalStores(
keys,
(key, callback) => this.queuedQuerySync.on(key, callback),
Expand Down Expand Up @@ -153,7 +155,7 @@ export class DebounceController {
this.queues.clear()
}

getQueuedQuery(key: string): string | null | undefined {
getQueuedQuery(key: string): Iterable<string> | null | undefined {
// The debounced queued values are more likely to be up-to-date
// than any updates pending in the throttle queue, which comes last
// in the update chain.
Expand Down
9 changes: 5 additions & 4 deletions packages/nuqs/src/lib/queues/throttle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { error } from '../errors'
import { timeout } from '../timeout'
import { withResolvers, type Resolvers } from '../with-resolvers'
import { defaultRateLimit } from './rate-limiting'
import { write } from '../search-params'

type UpdateMap = Map<string, string | null>
type UpdateMap = Map<string, Iterable<string> | null>
type TransitionSet = Set<React.TransitionStartFunction>
export type UpdateQueueAdapterContext = Pick<
AdapterInterface,
Expand All @@ -19,7 +20,7 @@ export type UpdateQueueAdapterContext = Pick<

export type UpdateQueuePushArgs = {
key: string
query: string | null
query: Iterable<string> | null
options: AdapterOptions & Pick<Options, 'startTransition'>
}

Expand Down Expand Up @@ -65,7 +66,7 @@ export class ThrottledQueue {
}
}

getQueuedQuery(key: string): string | null | undefined {
getQueuedQuery(key: string): Iterable<string> | null | undefined {
return this.updateMap.get(key)
}

Expand Down Expand Up @@ -185,7 +186,7 @@ export class ThrottledQueue {
if (value === null) {
search.delete(key)
} else {
search.set(key, value)
search = write(value, key, search)
}
}
if (processUrlSearchParams) {
Expand Down
9 changes: 4 additions & 5 deletions packages/nuqs/src/lib/safe-parse.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { Parser } from '../parsers'
import { warn } from './debug'

export function safeParse<T>(
parser: Parser<T>['parse'],
value: string,
export function safeParse<I, R>(
parser: (arg: I) => R,
value: I,
key?: string
): T | null {
): R | null {
try {
return parser(value)
} catch (error) {
Expand Down
19 changes: 19 additions & 0 deletions packages/nuqs/src/lib/search-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export function isEmpty(query: string | Iterable<string> | null): boolean {
return query === null || (Array.isArray(query) && query.length === 0)
}

export function write(
serialized: Iterable<string>,
key: string,
searchParams: URLSearchParams
): URLSearchParams {
if (typeof serialized === 'string') {
searchParams.set(key, serialized)
} else {
searchParams.delete(key)
for (const v of serialized) {
searchParams.append(key, v)
}
}
return searchParams
}
2 changes: 1 addition & 1 deletion packages/nuqs/src/lib/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createEmitter, type Emitter } from './emitter'

export type CrossHookSyncPayload = {
state: any
query: string | null
query: Iterable<string> | null
}

type EventMap = {
Expand Down
13 changes: 9 additions & 4 deletions packages/nuqs/src/loader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { UrlKeys } from './defs'
import type { inferParserType, ParserMap } from './parsers'
import { type inferParserType, type ParserMap } from './parsers'
import { isEmpty } from './lib/search-params'

export type LoaderInput =
| URL
Expand Down Expand Up @@ -96,14 +97,18 @@ export function createLoader<Parsers extends ParserMap>(
const result = {} as any
for (const [key, parser] of Object.entries(parsers)) {
const urlKey = urlKeys[key] ?? key
const query = searchParams.get(urlKey)
if (query === null) {
const query =
parser.type === 'multi'
? searchParams.getAll(urlKey)
: searchParams.get(urlKey)
if (isEmpty(query)) {
result[key] = parser.defaultValue ?? null
continue
}
let parsedValue
try {
parsedValue = parser.parse(query)
// we have properly narrowed `query` here, but TS doesn't keep track of that
parsedValue = parser.parse(query as string & readonly string[])
} catch (error) {
if (strict) {
throw new Error(
Expand Down
Loading