Skip to content

Commit 9c93ff2

Browse files
committed
chore: Replace history patching with uSP sync
1 parent fa0b732 commit 9c93ff2

File tree

4 files changed

+28
-98
lines changed

4 files changed

+28
-98
lines changed

packages/nuqs/src/sync.ts

-92
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import Mitt from 'mitt'
2-
import { debug } from './debug'
3-
import { error } from './errors'
4-
import { getQueuedValue } from './update-queue'
52

63
export const SYNC_EVENT_KEY = Symbol('__nuqs__SYNC__')
7-
export const NOSYNC_MARKER = '__nuqs__NO_SYNC__'
84
const NOTIFY_EVENT_KEY = Symbol('__nuqs__NOTIFY__')
95

106
export type QueryUpdateSource = 'internal' | 'external'
@@ -24,91 +20,3 @@ type EventMap = {
2420
}
2521

2622
export const emitter = Mitt<EventMap>()
27-
28-
declare global {
29-
interface History {
30-
__nuqs_patched?: string
31-
}
32-
}
33-
34-
if (typeof history === 'object') {
35-
patchHistory()
36-
}
37-
38-
function patchHistory() {
39-
// This is replaced with the package.json version by scripts/prepack.sh
40-
// after semantic-release has done updating the version number.
41-
const version = '0.0.0-inject-version-here'
42-
const patched = history.__nuqs_patched
43-
if (patched) {
44-
if (patched !== version) {
45-
console.error(error(409), patched, version)
46-
}
47-
return
48-
}
49-
debug('[nuqs] Patching history with %s', version)
50-
for (const method of ['pushState', 'replaceState'] as const) {
51-
const original = history[method].bind(history)
52-
history[method] = function nuqs_patchedHistory(
53-
state: any,
54-
title: string,
55-
url?: string | URL | null
56-
) {
57-
if (!url) {
58-
// Null URL is only used for state changes,
59-
// we're not interested in reacting to those.
60-
debug('[nuqs] history.%s(null) (%s) %O', method, title, state)
61-
return original(state, title, url)
62-
}
63-
const source = title === NOSYNC_MARKER ? 'internal' : 'external'
64-
const search = new URL(url, location.origin).searchParams
65-
debug('[nuqs] history.%s(%s) (%s) %O', method, url, source, state)
66-
// If someone else than our hooks have updated the URL,
67-
// send out a signal for them to sync their internal state.
68-
if (source === 'external') {
69-
for (const [key, value] of search.entries()) {
70-
const queueValue = getQueuedValue(key)
71-
if (queueValue !== null && queueValue !== value) {
72-
debug(
73-
'[nuqs] Overwrite detected for key: %s, Server: %s, queue: %s',
74-
key,
75-
value,
76-
queueValue
77-
)
78-
search.set(key, queueValue)
79-
}
80-
}
81-
// Here we're delaying application to next tick to avoid:
82-
// `Warning: useInsertionEffect must not schedule updates.`
83-
//
84-
// Because the emitter runs in sync, this would trigger
85-
// each hook's setInternalState updates, so we schedule
86-
// those after the current batch of events.
87-
// Because we don't know if the history method would
88-
// have been applied by then, we're also sending the
89-
// parsed query string to the hooks so they don't need
90-
// to rely on the URL being up to date.
91-
setTimeout(() => {
92-
debug(
93-
'[nuqs] External history.%s call: triggering sync with %s',
94-
method,
95-
search
96-
)
97-
emitter.emit(SYNC_EVENT_KEY, search)
98-
emitter.emit(NOTIFY_EVENT_KEY, { search, source })
99-
}, 0)
100-
} else {
101-
setTimeout(() => {
102-
emitter.emit(NOTIFY_EVENT_KEY, { search, source })
103-
}, 0)
104-
}
105-
return original(state, title === NOSYNC_MARKER ? '' : title, url)
106-
}
107-
}
108-
Object.defineProperty(history, '__nuqs_patched', {
109-
value: version,
110-
writable: false,
111-
enumerable: false,
112-
configurable: false
113-
})
114-
}

packages/nuqs/src/update-queue.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { NextRouter } from 'next/router'
22
import { debug } from './debug'
33
import type { Options, Router } from './defs'
44
import { error } from './errors'
5-
import { NOSYNC_MARKER } from './sync'
65
import { renderQueryString } from './url-encoding'
76
import { getDefaultThrottle } from './utils'
87

@@ -191,11 +190,7 @@ function flushUpdateQueue(router: Router): [URLSearchParams, null | unknown] {
191190
// In next@14.1.0, useSearchParams becomes reactive to shallow updates,
192191
// but only if passing `null` as the history state.
193192
null,
194-
// Our own updates have a marker to prevent syncing
195-
// when the URL changes (we've already sync'd them up
196-
// via `emitter.emit(key, newValue)` above, without
197-
// going through the parsers).
198-
NOSYNC_MARKER,
193+
'',
199194
url
200195
)
201196
if (options.scroll) {

packages/nuqs/src/useQueryState.ts

+12
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,18 @@ export function useQueryState<T = string>(
241241
initialSearchParams?.get(key) ?? null
242242
)
243243

244+
React.useEffect(() => {
245+
const query = initialSearchParams.get(key) ?? null
246+
if (query === queryRef.current) {
247+
return
248+
}
249+
const state = query === null ? null : safeParse(parse, query, key)
250+
debug('[nuqs `%s`] syncFromUseSearchParams %O', key, state)
251+
stateRef.current = state
252+
queryRef.current = query
253+
setInternalState(state)
254+
}, [initialSearchParams?.get(key), key])
255+
244256
// Sync all hooks together & with external URL changes
245257
React.useInsertionEffect(() => {
246258
function updateInternalState({ state, query }: CrossHookSyncPayload) {

packages/nuqs/src/useQueryStates.ts

+15
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,21 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
8888
initialSearchParams
8989
)
9090

91+
React.useEffect(() => {
92+
const state = parseMap(
93+
keyMap,
94+
initialSearchParams,
95+
queryRef.current,
96+
stateRef.current
97+
)
98+
setInternalState(state)
99+
}, [
100+
Object.keys(keyMap)
101+
.map(key => initialSearchParams?.get(key))
102+
.join('&'),
103+
keys
104+
])
105+
91106
// Sync all hooks together & with external URL changes
92107
React.useInsertionEffect(() => {
93108
function updateInternalState(state: V) {

0 commit comments

Comments
 (0)