Skip to content

Commit 9ceda53

Browse files
authored
ref: Enable history patching by default for React Router (#833)
1 parent 3d345cd commit 9ceda53

File tree

12 files changed

+41
-71
lines changed

12 files changed

+41
-71
lines changed

packages/docs/content/docs/options.mdx

+9-21
Original file line numberDiff line numberDiff line change
@@ -102,27 +102,15 @@ function Component() {
102102
This concept of _"shallow routing"_ is done via updates to the browser's
103103
[History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API).
104104

105-
While the `useOptimisticSearchParams` and the adapter itself can handle shallow URL
106-
updates triggered from state updater functions, for them to react to URL changes
107-
triggered by explicit calls to the History API (either by first or third party code),
108-
you'd have to enable sync:
109-
110-
```tsx
111-
// Export available in:
112-
// 'nuqs/adapters/remix'
113-
// 'nuqs/adapters/react-router/v6'
114-
// 'nuqs/adapters/react-router/v7'
115-
// 'nuqs/adapters/react'
116-
import { enableHistorySync } from 'nuqs/adapters/remix'
117-
118-
// Somewhere top-level (like app/root.tsx)
119-
enableHistorySync()
120-
```
121-
122-
Note that you may not need this if only using your framework's router.
123-
124-
It is opt-in as it patches the History APIs, which can have side effects
125-
if third party code does it too.
105+
<Callout title="Why not using shouldRevalidate?">
106+
[`shouldRevalidate`](https://reactrouter.com/start/framework/route-module#shouldrevalidate)
107+
is the idomatic way of opting out of running loaders on navigation, but nuqs uses
108+
the opposite approach: opting in to running loaders only when needed.
109+
110+
In order to avoid specifying `shouldRevalidate` for every route, nuqs chose to
111+
patch the history methods to enable shallow routing by default (on its own updates)
112+
in React Router based frameworks.
113+
</Callout>
126114

127115
## Scroll
128116

packages/e2e/react-router/v6/src/react-router.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { enableHistorySync, NuqsAdapter } from 'nuqs/adapters/react-router/v6'
1+
import { NuqsAdapter } from 'nuqs/adapters/react-router/v6'
22
import {
33
createBrowserRouter,
44
createRoutesFromElements,
@@ -7,8 +7,6 @@ import {
77
} from 'react-router-dom'
88
import RootLayout from './layout'
99

10-
enableHistorySync()
11-
1210
// Adapt the RRv7 / Remix default export for component into a Component export for v6
1311
function load(mod: Promise<{ default: any; [otherExports: string]: any }>) {
1412
return () =>

packages/e2e/react-router/v7/app/root.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { enableHistorySync, NuqsAdapter } from 'nuqs/adapters/react-router/v7'
1+
import { NuqsAdapter } from 'nuqs/adapters/react-router/v7'
22
import {
33
isRouteErrorResponse,
44
Links,
@@ -8,8 +8,6 @@ import {
88
ScrollRestoration
99
} from 'react-router'
1010

11-
enableHistorySync()
12-
1311
import type { Route } from './+types/root'
1412

1513
export function Layout({ children }: { children: React.ReactNode }) {

packages/e2e/remix/app/root.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { Links, Meta, Scripts, ScrollRestoration } from '@remix-run/react'
2-
import { enableHistorySync, NuqsAdapter } from 'nuqs/adapters/remix'
2+
import { NuqsAdapter } from 'nuqs/adapters/remix'
33
import RootLayout from './layout'
44

5-
enableHistorySync()
6-
75
export function Layout({ children }: { children: React.ReactNode }) {
86
return (
97
<html lang="en">

packages/nuqs/src/adapters/lib/patch-history.ts

+12
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,24 @@ export function patchHistory(
5151
if (history.nuqs?.adapters?.includes(adapter)) {
5252
return
5353
}
54+
let lastSearchSeen = typeof location === 'object' ? location.search : ''
55+
56+
emitter.on('update', search => {
57+
lastSearchSeen = search.toString()
58+
})
59+
5460
debug(
5561
'[nuqs %s] Patching history (%s adapter)',
5662
'0.0.0-inject-version-here',
5763
adapter
5864
)
5965
function sync(url: URL | string) {
66+
try {
67+
const newSearch = new URL(url, location.origin).search
68+
if (newSearch === lastSearchSeen) {
69+
return
70+
}
71+
} catch {}
6072
try {
6173
emitter.emit('update', getSearchParams(url))
6274
} catch (e) {

packages/nuqs/src/adapters/lib/react-router.ts

-3
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,6 @@ export function createReactRouterBasedAdapter(
9292
window.removeEventListener('popstate', onPopState)
9393
}
9494
}, [])
95-
useEffect(() => {
96-
emitter.emit('update', serverSearchParams)
97-
}, [serverSearchParams])
9895
return searchParams
9996
}
10097
/**

packages/nuqs/src/adapters/react-router.ts

-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,4 @@
11
export {
2-
/**
3-
* @deprecated This import will be removed in nuqs@3.0.0.
4-
*
5-
* Please pin your version of React Router in the import:
6-
* - `nuqs/adapters/react-router/v6`
7-
* - `nuqs/adapters/react-router/v7`.
8-
*
9-
* Note: this deprecated import (`nuqs/adapters/react-router`) is for React Router v6 only.
10-
*/
11-
enableHistorySync,
122
/**
133
* @deprecated This import will be removed in nuqs@3.0.0.
144
*

packages/nuqs/src/adapters/react-router/v6.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const {
1212
useSearchParams
1313
)
1414

15-
export { enableHistorySync, useOptimisticSearchParams }
15+
export { useOptimisticSearchParams }
1616

1717
export const NuqsAdapter = createAdapterProvider(useNuqsReactRouterV6Adapter)
18+
19+
enableHistorySync()

packages/nuqs/src/adapters/react-router/v7.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const {
1212
useSearchParams
1313
)
1414

15-
export { enableHistorySync, useOptimisticSearchParams }
15+
export { useOptimisticSearchParams }
1616

1717
export const NuqsAdapter = createAdapterProvider(useNuqsReactRouterV7Adapter)
18+
19+
enableHistorySync()

packages/nuqs/src/adapters/remix.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const {
88
useOptimisticSearchParams
99
} = createReactRouterBasedAdapter('remix', useNavigate, useSearchParams)
1010

11-
export { enableHistorySync, useOptimisticSearchParams }
11+
export { useOptimisticSearchParams }
1212

1313
export const NuqsAdapter = createAdapterProvider(useNuqsRemixAdapter)
14+
15+
enableHistorySync()

packages/nuqs/src/useQueryState.ts

+4-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
import {
2-
useCallback,
3-
useEffect,
4-
useInsertionEffect,
5-
useRef,
6-
useState
7-
} from 'react'
1+
import { useCallback, useEffect, useRef, useState } from 'react'
82
import { useAdapter } from './adapters/lib/context'
93
import { debug } from './debug'
104
import type { Options } from './defs'
@@ -260,7 +254,7 @@ export function useQueryState<T = string>(
260254
}, [initialSearchParams?.get(key), key])
261255

262256
// Sync all hooks together & with external URL changes
263-
useInsertionEffect(() => {
257+
useEffect(() => {
264258
function updateInternalState({ state, query }: CrossHookSyncPayload) {
265259
debug('[nuqs `%s`] updateInternalState %O', key, state)
266260
stateRef.current = state
@@ -288,7 +282,7 @@ export function useQueryState<T = string>(
288282
) {
289283
newValue = null
290284
}
291-
queryRef.current = enqueueQueryStringUpdate(key, newValue, serialize, {
285+
const query = enqueueQueryStringUpdate(key, newValue, serialize, {
292286
// Call-level options take precedence over hook declaration options.
293287
history: options.history ?? history,
294288
shallow: options.shallow ?? shallow,
@@ -297,7 +291,7 @@ export function useQueryState<T = string>(
297291
startTransition: options.startTransition ?? startTransition
298292
})
299293
// Sync all hooks state (including this one)
300-
emitter.emit(key, { state: newValue, query: queryRef.current })
294+
emitter.emit(key, { state: newValue, query })
301295
return scheduleFlushToURL(adapter)
302296
},
303297
[key, history, shallow, scroll, throttleMs, startTransition, adapter]

packages/nuqs/src/useQueryStates.ts

+4-15
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
import {
2-
useCallback,
3-
useEffect,
4-
useInsertionEffect,
5-
useMemo,
6-
useRef,
7-
useState
8-
} from 'react'
1+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
92
import { useAdapter } from './adapters/lib/context'
103
import { debug } from './debug'
114
import type { Nullable, Options, UrlKeys } from './defs'
@@ -139,7 +132,7 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
139132
])
140133

141134
// Sync all hooks together & with external URL changes
142-
useInsertionEffect(() => {
135+
useEffect(() => {
143136
function updateInternalState(state: V) {
144137
debug('[nuq+ `%s`] updateInternalState %O', stateKeys, state)
145138
stateRef.current = state
@@ -216,8 +209,7 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
216209
) {
217210
value = null
218211
}
219-
220-
queryRef.current[urlKey] = enqueueQueryStringUpdate(
212+
const query = enqueueQueryStringUpdate(
221213
urlKey,
222214
value,
223215
parser.serialize ?? String,
@@ -235,10 +227,7 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
235227
startTransition
236228
}
237229
)
238-
emitter.emit(urlKey, {
239-
state: value,
240-
query: queryRef.current[urlKey] ?? null
241-
})
230+
emitter.emit(urlKey, { state: value, query })
242231
}
243232
return scheduleFlushToURL(adapter)
244233
},

0 commit comments

Comments
 (0)