Skip to content

Commit b7e371d

Browse files
committedAug 30, 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 98d5d2c commit b7e371d

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,
@@ -101,34 +101,34 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
101101
}
102102
const handlers = Object.keys(keyMap).reduce(
103103
(handlers, key) => {
104-
handlers[key as keyof V] = (value: any) => {
105-
const { defaultValue, serialize = String } = keyMap[key]!
104+
handlers[key as keyof V] = ({ state, query }: CrossHookSyncPayload) => {
105+
const { defaultValue } = keyMap[key]!
106106
// Note: cannot mutate in-place, the object ref must change
107107
// for the subsequent setState to pick it up.
108108
stateRef.current = {
109109
...stateRef.current,
110-
[key as keyof V]: value ?? defaultValue ?? null
110+
[key as keyof V]: state ?? defaultValue ?? null
111111
}
112-
queryRef.current[key] = value === null ? null : serialize(value)
112+
queryRef.current[key] = query
113113
debug(
114114
'[nuq+ `%s`] Cross-hook key sync %s: %O (default: %O). Resolved: %O',
115115
keys,
116116
key,
117-
value,
117+
state,
118118
defaultValue,
119119
stateRef.current
120120
)
121121
updateInternalState(stateRef.current)
122122
}
123123
return handlers
124124
},
125-
{} as Record<keyof V, any>
125+
{} as Record<keyof V, (payload: CrossHookSyncPayload) => void>
126126
)
127127

128128
emitter.on(SYNC_EVENT_KEY, syncFromURL)
129129
for (const key of Object.keys(keyMap)) {
130130
debug('[nuq+ `%s`] Subscribing to sync for `%s`', keys, key)
131-
emitter.on(key, handlers[key])
131+
emitter.on(key, handlers[key]!)
132132
}
133133
return () => {
134134
emitter.off(SYNC_EVENT_KEY, syncFromURL)
@@ -159,7 +159,7 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
159159
) {
160160
value = null
161161
}
162-
emitter.emit(key, value)
162+
163163
queryRef.current[key] = enqueueQueryStringUpdate(
164164
key,
165165
value,
@@ -173,6 +173,10 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
173173
startTransition: options.startTransition ?? startTransition
174174
}
175175
)
176+
emitter.emit(key, {
177+
state: value,
178+
query: queryRef.current[key] ?? null
179+
})
176180
}
177181
return scheduleFlushToURL(router)
178182
},

0 commit comments

Comments
 (0)