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) },