diff --git a/packages/docs/content/docs/options.mdx b/packages/docs/content/docs/options.mdx
index d6dae49e..7af59300 100644
--- a/packages/docs/content/docs/options.mdx
+++ b/packages/docs/content/docs/options.mdx
@@ -102,27 +102,15 @@ function Component() {
This concept of _"shallow routing"_ is done via updates to the browser's
[History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API).
-While the `useOptimisticSearchParams` and the adapter itself can handle shallow URL
-updates triggered from state updater functions, for them to react to URL changes
-triggered by explicit calls to the History API (either by first or third party code),
-you'd have to enable sync:
-
-```tsx
-// Export available in:
-// 'nuqs/adapters/remix'
-// 'nuqs/adapters/react-router/v6'
-// 'nuqs/adapters/react-router/v7'
-// 'nuqs/adapters/react'
-import { enableHistorySync } from 'nuqs/adapters/remix'
-
-// Somewhere top-level (like app/root.tsx)
-enableHistorySync()
-```
-
-Note that you may not need this if only using your framework's router.
-
-It is opt-in as it patches the History APIs, which can have side effects
-if third party code does it too.
+
+ [`shouldRevalidate`](https://reactrouter.com/start/framework/route-module#shouldrevalidate)
+ is the idomatic way of opting out of running loaders on navigation, but nuqs uses
+ the opposite approach: opting in to running loaders only when needed.
+
+ In order to avoid specifying `shouldRevalidate` for every route, nuqs chose to
+ patch the history methods to enable shallow routing by default (on its own updates)
+ in React Router based frameworks.
+
## Scroll
diff --git a/packages/e2e/react-router/v6/src/react-router.tsx b/packages/e2e/react-router/v6/src/react-router.tsx
index 32d8ab3c..ecb2deef 100644
--- a/packages/e2e/react-router/v6/src/react-router.tsx
+++ b/packages/e2e/react-router/v6/src/react-router.tsx
@@ -1,4 +1,4 @@
-import { enableHistorySync, NuqsAdapter } from 'nuqs/adapters/react-router/v6'
+import { NuqsAdapter } from 'nuqs/adapters/react-router/v6'
import {
createBrowserRouter,
createRoutesFromElements,
@@ -7,8 +7,6 @@ import {
} from 'react-router-dom'
import RootLayout from './layout'
-enableHistorySync()
-
// Adapt the RRv7 / Remix default export for component into a Component export for v6
function load(mod: Promise<{ default: any; [otherExports: string]: any }>) {
return () =>
diff --git a/packages/e2e/react-router/v7/app/root.tsx b/packages/e2e/react-router/v7/app/root.tsx
index c5bb7241..9344e74a 100644
--- a/packages/e2e/react-router/v7/app/root.tsx
+++ b/packages/e2e/react-router/v7/app/root.tsx
@@ -1,4 +1,4 @@
-import { enableHistorySync, NuqsAdapter } from 'nuqs/adapters/react-router/v7'
+import { NuqsAdapter } from 'nuqs/adapters/react-router/v7'
import {
isRouteErrorResponse,
Links,
@@ -8,8 +8,6 @@ import {
ScrollRestoration
} from 'react-router'
-enableHistorySync()
-
import type { Route } from './+types/root'
export function Layout({ children }: { children: React.ReactNode }) {
diff --git a/packages/e2e/remix/app/root.tsx b/packages/e2e/remix/app/root.tsx
index f35600e6..9948b22f 100644
--- a/packages/e2e/remix/app/root.tsx
+++ b/packages/e2e/remix/app/root.tsx
@@ -1,9 +1,7 @@
import { Links, Meta, Scripts, ScrollRestoration } from '@remix-run/react'
-import { enableHistorySync, NuqsAdapter } from 'nuqs/adapters/remix'
+import { NuqsAdapter } from 'nuqs/adapters/remix'
import RootLayout from './layout'
-enableHistorySync()
-
export function Layout({ children }: { children: React.ReactNode }) {
return (
diff --git a/packages/nuqs/src/adapters/lib/patch-history.ts b/packages/nuqs/src/adapters/lib/patch-history.ts
index d3404a1a..22c12b6a 100644
--- a/packages/nuqs/src/adapters/lib/patch-history.ts
+++ b/packages/nuqs/src/adapters/lib/patch-history.ts
@@ -51,12 +51,24 @@ export function patchHistory(
if (history.nuqs?.adapters?.includes(adapter)) {
return
}
+ let lastSearchSeen = typeof location === 'object' ? location.search : ''
+
+ emitter.on('update', search => {
+ lastSearchSeen = search.toString()
+ })
+
debug(
'[nuqs %s] Patching history (%s adapter)',
'0.0.0-inject-version-here',
adapter
)
function sync(url: URL | string) {
+ try {
+ const newSearch = new URL(url, location.origin).search
+ if (newSearch === lastSearchSeen) {
+ return
+ }
+ } catch {}
try {
emitter.emit('update', getSearchParams(url))
} catch (e) {
diff --git a/packages/nuqs/src/adapters/lib/react-router.ts b/packages/nuqs/src/adapters/lib/react-router.ts
index 6f1f829f..846d6758 100644
--- a/packages/nuqs/src/adapters/lib/react-router.ts
+++ b/packages/nuqs/src/adapters/lib/react-router.ts
@@ -92,9 +92,6 @@ export function createReactRouterBasedAdapter(
window.removeEventListener('popstate', onPopState)
}
}, [])
- useEffect(() => {
- emitter.emit('update', serverSearchParams)
- }, [serverSearchParams])
return searchParams
}
/**
diff --git a/packages/nuqs/src/adapters/react-router.ts b/packages/nuqs/src/adapters/react-router.ts
index ed41f158..04d429b7 100644
--- a/packages/nuqs/src/adapters/react-router.ts
+++ b/packages/nuqs/src/adapters/react-router.ts
@@ -1,14 +1,4 @@
export {
- /**
- * @deprecated This import will be removed in nuqs@3.0.0.
- *
- * Please pin your version of React Router in the import:
- * - `nuqs/adapters/react-router/v6`
- * - `nuqs/adapters/react-router/v7`.
- *
- * Note: this deprecated import (`nuqs/adapters/react-router`) is for React Router v6 only.
- */
- enableHistorySync,
/**
* @deprecated This import will be removed in nuqs@3.0.0.
*
diff --git a/packages/nuqs/src/adapters/react-router/v6.ts b/packages/nuqs/src/adapters/react-router/v6.ts
index 7acc23d3..f634fc7f 100644
--- a/packages/nuqs/src/adapters/react-router/v6.ts
+++ b/packages/nuqs/src/adapters/react-router/v6.ts
@@ -12,6 +12,8 @@ const {
useSearchParams
)
-export { enableHistorySync, useOptimisticSearchParams }
+export { useOptimisticSearchParams }
export const NuqsAdapter = createAdapterProvider(useNuqsReactRouterV6Adapter)
+
+enableHistorySync()
diff --git a/packages/nuqs/src/adapters/react-router/v7.ts b/packages/nuqs/src/adapters/react-router/v7.ts
index 1c46bb41..b2cc186f 100644
--- a/packages/nuqs/src/adapters/react-router/v7.ts
+++ b/packages/nuqs/src/adapters/react-router/v7.ts
@@ -12,6 +12,8 @@ const {
useSearchParams
)
-export { enableHistorySync, useOptimisticSearchParams }
+export { useOptimisticSearchParams }
export const NuqsAdapter = createAdapterProvider(useNuqsReactRouterV7Adapter)
+
+enableHistorySync()
diff --git a/packages/nuqs/src/adapters/remix.ts b/packages/nuqs/src/adapters/remix.ts
index db735886..79af67d1 100644
--- a/packages/nuqs/src/adapters/remix.ts
+++ b/packages/nuqs/src/adapters/remix.ts
@@ -8,6 +8,8 @@ const {
useOptimisticSearchParams
} = createReactRouterBasedAdapter('remix', useNavigate, useSearchParams)
-export { enableHistorySync, useOptimisticSearchParams }
+export { useOptimisticSearchParams }
export const NuqsAdapter = createAdapterProvider(useNuqsRemixAdapter)
+
+enableHistorySync()
diff --git a/packages/nuqs/src/useQueryState.ts b/packages/nuqs/src/useQueryState.ts
index faa9da55..15d8bf92 100644
--- a/packages/nuqs/src/useQueryState.ts
+++ b/packages/nuqs/src/useQueryState.ts
@@ -1,10 +1,4 @@
-import {
- useCallback,
- useEffect,
- useInsertionEffect,
- useRef,
- useState
-} from 'react'
+import { useCallback, useEffect, useRef, useState } from 'react'
import { useAdapter } from './adapters/lib/context'
import { debug } from './debug'
import type { Options } from './defs'
@@ -260,7 +254,7 @@ export function useQueryState(
}, [initialSearchParams?.get(key), key])
// Sync all hooks together & with external URL changes
- useInsertionEffect(() => {
+ useEffect(() => {
function updateInternalState({ state, query }: CrossHookSyncPayload) {
debug('[nuqs `%s`] updateInternalState %O', key, state)
stateRef.current = state
@@ -288,7 +282,7 @@ export function useQueryState(
) {
newValue = null
}
- queryRef.current = enqueueQueryStringUpdate(key, newValue, serialize, {
+ const query = enqueueQueryStringUpdate(key, newValue, serialize, {
// Call-level options take precedence over hook declaration options.
history: options.history ?? history,
shallow: options.shallow ?? shallow,
@@ -297,7 +291,7 @@ export function useQueryState(
startTransition: options.startTransition ?? startTransition
})
// Sync all hooks state (including this one)
- emitter.emit(key, { state: newValue, query: queryRef.current })
+ emitter.emit(key, { state: newValue, query })
return scheduleFlushToURL(adapter)
},
[key, history, shallow, scroll, throttleMs, startTransition, adapter]
diff --git a/packages/nuqs/src/useQueryStates.ts b/packages/nuqs/src/useQueryStates.ts
index 23a81ff0..7daa917c 100644
--- a/packages/nuqs/src/useQueryStates.ts
+++ b/packages/nuqs/src/useQueryStates.ts
@@ -1,11 +1,4 @@
-import {
- useCallback,
- useEffect,
- useInsertionEffect,
- useMemo,
- useRef,
- useState
-} from 'react'
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useAdapter } from './adapters/lib/context'
import { debug } from './debug'
import type { Nullable, Options, UrlKeys } from './defs'
@@ -139,7 +132,7 @@ export function useQueryStates(
])
// Sync all hooks together & with external URL changes
- useInsertionEffect(() => {
+ useEffect(() => {
function updateInternalState(state: V) {
debug('[nuq+ `%s`] updateInternalState %O', stateKeys, state)
stateRef.current = state
@@ -216,8 +209,7 @@ export function useQueryStates(
) {
value = null
}
-
- queryRef.current[urlKey] = enqueueQueryStringUpdate(
+ const query = enqueueQueryStringUpdate(
urlKey,
value,
parser.serialize ?? String,
@@ -235,10 +227,7 @@ export function useQueryStates(
startTransition
}
)
- emitter.emit(urlKey, {
- state: value,
- query: queryRef.current[urlKey] ?? null
- })
+ emitter.emit(urlKey, { state: value, query })
}
return scheduleFlushToURL(adapter)
},