()
diff --git a/packages/nuqs/src/useQueryStates.test.tsx b/packages/nuqs/src/useQueryStates.test.tsx
index 6168a3ee3..091e771e9 100644
--- a/packages/nuqs/src/useQueryStates.test.tsx
+++ b/packages/nuqs/src/useQueryStates.test.tsx
@@ -192,7 +192,7 @@ describe('useQueryStates: referential equality', () => {
expect(result.current[0].str).toBe('foo')
rerender({ defaultValue: 'b' })
const [state] = result.current
- expect(state.str).toBe('b')
+ expect(state.str).toBe('foo')
expect(state.obj).toBe(defaults.obj)
expect(state.arr).toBe(defaults.arr)
expect(state.arr[0]).toBe(defaults.arr[0])
@@ -266,6 +266,39 @@ describe('useQueryStates: urlKeys remapping', () => {
})
})
+describe('useQueryStates: defaultValue', () => {
+ it('should read the same default value for multiple usages of the same parser', () => {
+ function TestComponent({
+ id,
+ defaultValue
+ }: {
+ id: string
+ defaultValue: number
+ }) {
+ const [value] = useQueryStates({
+ a: parseAsInteger.withDefault(defaultValue)
+ })
+
+ return (
+
+ {id} value: {value.a}
+
+ )
+ }
+ const result = render(
+
+
+
+
,
+ {
+ wrapper: withNuqsTestingAdapter()
+ }
+ )
+ expect(result.getByText('first value: 5')).toBeInTheDocument()
+ expect(result.getByText('second value: 5')).toBeInTheDocument()
+ })
+})
+
describe('useQueryStates: clearOnDefault', () => {
it('honors clearOnDefault: true by default', async () => {
const onUrlUpdate = vi.fn()
diff --git a/packages/nuqs/src/useQueryStates.ts b/packages/nuqs/src/useQueryStates.ts
index 8d51d0a54..8b84cce00 100644
--- a/packages/nuqs/src/useQueryStates.ts
+++ b/packages/nuqs/src/useQueryStates.ts
@@ -1,8 +1,10 @@
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
import {
+ type DefaultValueStore,
useAdapter,
useAdapterDefaultOptions,
- useAdapterProcessUrlSearchParams
+ useAdapterProcessUrlSearchParams,
+ useDefaultValueStore
} from './adapters/lib/context'
import type { Nullable, Options, UrlKeys } from './defs'
import { compareQuery } from './lib/compare'
@@ -76,6 +78,7 @@ export function useQueryStates(
const hookId = useId()
const defaultOptions = useAdapterDefaultOptions()
const processUrlSearchParams = useAdapterProcessUrlSearchParams()
+ const defaultValueStore = useDefaultValueStore()
const {
history = 'replace',
@@ -100,17 +103,14 @@ export function useQueryStates(
const adapter = useAdapter(Object.values(resolvedUrlKeys))
const initialSearchParams = adapter.searchParams
const queryRef = useRef>({})
- const defaultValues = useMemo(
- () =>
- Object.fromEntries(
- Object.keys(keyMap).map(key => [key, keyMap[key]!.defaultValue ?? null])
- ) as Values,
- [
- Object.values(keyMap)
- .map(({ defaultValue }) => defaultValue)
- .join(',')
- ]
- )
+
+ // lazily initialize defaultValues in store
+ for (const [stateKey, { defaultValue }] of Object.entries(keyMap)) {
+ if (!(stateKey in defaultValueStore)) {
+ defaultValueStore[stateKey] = defaultValue ?? null
+ }
+ }
+
const queuedQueries = debounceController.useQueuedQueries(
Object.values(resolvedUrlKeys)
)
@@ -191,7 +191,6 @@ export function useQueryStates(
useEffect(() => {
function updateInternalState(state: V) {
debug('[nuq+ %s `%s`] updateInternalState %O', hookId, stateKeys, state)
- stateRef.current = state
setInternalState(state)
}
const handlers = Object.keys(keyMap).reduce(
@@ -200,7 +199,7 @@ export function useQueryStates(
state,
query
}: CrossHookSyncPayload) => {
- const { defaultValue } = keyMap[stateKey]!
+ const defaultValue = defaultValueStore[stateKey]
const urlKey = resolvedUrlKeys[stateKey]!
// Note: cannot mutate in-place, the object ref must change
// for the subsequent setState to pick it up.
@@ -257,7 +256,7 @@ export function useQueryStates(
const newState: Partial> =
typeof stateUpdater === 'function'
? (stateUpdater(
- applyDefaultValues(stateRef.current, defaultValues)
+ applyDefaultValues(stateRef.current, defaultValueStore)
) ?? nullMap)
: (stateUpdater ?? nullMap)
debug('[nuq+ %s `%s`] setState: %O', hookId, stateKeys, newState)
@@ -358,13 +357,17 @@ export function useQueryStates(
adapter.getSearchParamsSnapshot,
adapter.rateLimitFactor,
processUrlSearchParams,
- defaultValues
+ defaultValueStore
]
)
const outputState = useMemo(
- () => applyDefaultValues(internalState, defaultValues),
- [internalState, defaultValues]
+ () =>
+ applyDefaultValues(
+ internalState,
+ defaultValueStore as Partial>
+ ),
+ [internalState, defaultValueStore]
)
return [outputState, update]
}
@@ -430,7 +433,7 @@ function parseMap(
function applyDefaultValues(
state: NullableValues,
- defaults: Partial>
+ defaults: DefaultValueStore
) {
return Object.fromEntries(
Object.keys(state).map(key => [key, state[key] ?? defaults[key] ?? null])