Skip to content

Commit 00beeeb

Browse files
committedSep 2, 2024
ref: Carry state & serialized query across hook sync
So that internal refs can be synced together without calling the serializer on each reception site.
1 parent 850ca5e commit 00beeeb

File tree

3 files changed

+34
-27
lines changed

3 files changed

+34
-27
lines changed
 

Diff for: ‎packages/nuqs/src/sync.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@ export type QueryUpdateNotificationArgs = {
1212
search: URLSearchParams
1313
source: QueryUpdateSource
1414
}
15+
export type CrossHookSyncPayload = {
16+
state: any
17+
query: string | null
18+
}
1519

1620
type EventMap = {
1721
[SYNC_EVENT_KEY]: URLSearchParams
1822
[NOTIFY_EVENT_KEY]: QueryUpdateNotificationArgs
19-
[key: string]: any
23+
[key: string]: CrossHookSyncPayload
2024
}
2125

2226
export const emitter = Mitt<EventMap>()

Diff for: ‎packages/nuqs/src/useQueryState.ts

+16-17
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react'
33
import { debug } from './debug'
44
import type { Options } from './defs'
55
import type { Parser } from './parsers'
6-
import { SYNC_EVENT_KEY, emitter } from './sync'
6+
import { SYNC_EVENT_KEY, emitter, type CrossHookSyncPayload } from './sync'
77
import {
88
FLUSH_RATE_LIMIT_MS,
99
enqueueQueryStringUpdate,
@@ -225,12 +225,12 @@ export function useQueryState<T = string>(
225225
const router = useRouter()
226226
// Not reactive, but available on the server and on page load
227227
const initialSearchParams = useSearchParams()
228-
const valueRef = React.useRef<string | null>(null)
228+
const queryRef = React.useRef<string | null>(null)
229229
const [internalState, setInternalState] = React.useState<T | null>(() => {
230230
const queueValue = getQueuedValue(key)
231231
const urlValue = initialSearchParams?.get(key) ?? null
232232
const value = queueValue ?? urlValue
233-
valueRef.current = value
233+
queryRef.current = value
234234
return value === null ? null : safeParse(parse, value, key)
235235
})
236236
const stateRef = React.useRef(internalState)
@@ -247,34 +247,33 @@ export function useQueryState<T = string>(
247247
if (window.next?.version !== '14.0.3') {
248248
return
249249
}
250-
const value = initialSearchParams.get(key) ?? null
251-
if (value === valueRef.current) {
250+
const query = initialSearchParams.get(key) ?? null
251+
if (query === queryRef.current) {
252252
return
253253
}
254-
const state = value === null ? null : safeParse(parse, value, key)
254+
const state = query === null ? null : safeParse(parse, query, key)
255255
debug('[nuqs `%s`] syncFromUseSearchParams %O', key, state)
256256
stateRef.current = state
257-
valueRef.current = value
257+
queryRef.current = query
258258
setInternalState(state)
259259
}, [initialSearchParams?.get(key), key])
260260

261261
// Sync all hooks together & with external URL changes
262262
React.useInsertionEffect(() => {
263-
function updateInternalState(state: T | null) {
263+
function updateInternalState({ state, query }: CrossHookSyncPayload) {
264264
debug('[nuqs `%s`] updateInternalState %O', key, state)
265265
stateRef.current = state
266-
valueRef.current = state === null ? null : serialize(state)
266+
queryRef.current = query
267267
setInternalState(state)
268268
}
269269
function syncFromURL(search: URLSearchParams) {
270-
const value = search.get(key) ?? null
271-
if (value === valueRef.current) {
270+
const query = search.get(key)
271+
if (query === queryRef.current) {
272272
return
273273
}
274-
const state = value === null ? null : safeParse(parse, value, key)
274+
const state = query === null ? null : safeParse(parse, query, key)
275275
debug('[nuqs `%s`] syncFromURL %O', key, state)
276-
updateInternalState(state)
277-
valueRef.current = value
276+
updateInternalState({ state, query })
278277
}
279278
debug('[nuqs `%s`] subscribing to sync', key)
280279
emitter.on(SYNC_EVENT_KEY, syncFromURL)
@@ -299,16 +298,16 @@ export function useQueryState<T = string>(
299298
) {
300299
newValue = null
301300
}
302-
// Sync all hooks state (including this one)
303-
emitter.emit(key, newValue)
304-
valueRef.current = enqueueQueryStringUpdate(key, newValue, serialize, {
301+
queryRef.current = enqueueQueryStringUpdate(key, newValue, serialize, {
305302
// Call-level options take precedence over hook declaration options.
306303
history: options.history ?? history,
307304
shallow: options.shallow ?? shallow,
308305
scroll: options.scroll ?? scroll,
309306
throttleMs: options.throttleMs ?? throttleMs,
310307
startTransition: options.startTransition ?? startTransition
311308
})
309+
// Sync all hooks state (including this one)
310+
emitter.emit(key, { state: newValue, query: queryRef.current })
312311
return scheduleFlushToURL(router)
313312
},
314313
[key, history, shallow, scroll, throttleMs, startTransition]

Diff for: ‎packages/nuqs/src/useQueryStates.ts

+13-9
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import React from 'react'
77
import { debug } from './debug'
88
import type { Nullable, Options } from './defs'
99
import type { Parser } from './parsers'
10-
import { SYNC_EVENT_KEY, emitter } from './sync'
10+
import { SYNC_EVENT_KEY, emitter, type CrossHookSyncPayload } from './sync'
1111
import {
1212
FLUSH_RATE_LIMIT_MS,
1313
enqueueQueryStringUpdate,
@@ -102,34 +102,34 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
102102
}
103103
const handlers = Object.keys(keyMap).reduce(
104104
(handlers, key) => {
105-
handlers[key as keyof V] = (value: any) => {
106-
const { defaultValue, serialize = String } = keyMap[key]!
105+
handlers[key as keyof V] = ({ state, query }: CrossHookSyncPayload) => {
106+
const { defaultValue } = keyMap[key]!
107107
// Note: cannot mutate in-place, the object ref must change
108108
// for the subsequent setState to pick it up.
109109
stateRef.current = {
110110
...stateRef.current,
111-
[key as keyof V]: value ?? defaultValue ?? null
111+
[key as keyof V]: state ?? defaultValue ?? null
112112
}
113-
queryRef.current[key] = value === null ? null : serialize(value)
113+
queryRef.current[key] = query
114114
debug(
115115
'[nuq+ `%s`] Cross-hook key sync %s: %O (default: %O). Resolved: %O',
116116
keys,
117117
key,
118-
value,
118+
state,
119119
defaultValue,
120120
stateRef.current
121121
)
122122
updateInternalState(stateRef.current)
123123
}
124124
return handlers
125125
},
126-
{} as Record<keyof V, any>
126+
{} as Record<keyof V, (payload: CrossHookSyncPayload) => void>
127127
)
128128

129129
emitter.on(SYNC_EVENT_KEY, syncFromURL)
130130
for (const key of Object.keys(keyMap)) {
131131
debug('[nuq+ `%s`] Subscribing to sync for `%s`', keys, key)
132-
emitter.on(key, handlers[key])
132+
emitter.on(key, handlers[key]!)
133133
}
134134
return () => {
135135
emitter.off(SYNC_EVENT_KEY, syncFromURL)
@@ -166,7 +166,7 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
166166
) {
167167
value = null
168168
}
169-
emitter.emit(key, value)
169+
170170
queryRef.current[key] = enqueueQueryStringUpdate(
171171
key,
172172
value,
@@ -185,6 +185,10 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
185185
startTransition
186186
}
187187
)
188+
emitter.emit(key, {
189+
state: value,
190+
query: queryRef.current[key] ?? null
191+
})
188192
}
189193
return scheduleFlushToURL(router)
190194
},

0 commit comments

Comments
 (0)