Skip to content

Commit

Permalink
ref: Enable history patching by default for React Router (#833)
Browse files Browse the repository at this point in the history
  • Loading branch information
franky47 authored Dec 31, 2024
1 parent 3d345cd commit 9ceda53
Show file tree
Hide file tree
Showing 12 changed files with 41 additions and 71 deletions.
30 changes: 9 additions & 21 deletions packages/docs/content/docs/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<Callout title="Why not using shouldRevalidate?">
[`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.
</Callout>

## Scroll

Expand Down
4 changes: 1 addition & 3 deletions packages/e2e/react-router/v6/src/react-router.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { enableHistorySync, NuqsAdapter } from 'nuqs/adapters/react-router/v6'
import { NuqsAdapter } from 'nuqs/adapters/react-router/v6'
import {
createBrowserRouter,
createRoutesFromElements,
Expand All @@ -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 () =>
Expand Down
4 changes: 1 addition & 3 deletions packages/e2e/react-router/v7/app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { enableHistorySync, NuqsAdapter } from 'nuqs/adapters/react-router/v7'
import { NuqsAdapter } from 'nuqs/adapters/react-router/v7'
import {
isRouteErrorResponse,
Links,
Expand All @@ -8,8 +8,6 @@ import {
ScrollRestoration
} from 'react-router'

enableHistorySync()

import type { Route } from './+types/root'

export function Layout({ children }: { children: React.ReactNode }) {
Expand Down
4 changes: 1 addition & 3 deletions packages/e2e/remix/app/root.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<html lang="en">
Expand Down
12 changes: 12 additions & 0 deletions packages/nuqs/src/adapters/lib/patch-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 0 additions & 3 deletions packages/nuqs/src/adapters/lib/react-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,6 @@ export function createReactRouterBasedAdapter(
window.removeEventListener('popstate', onPopState)
}
}, [])
useEffect(() => {
emitter.emit('update', serverSearchParams)
}, [serverSearchParams])
return searchParams
}
/**
Expand Down
10 changes: 0 additions & 10 deletions packages/nuqs/src/adapters/react-router.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
export {
/**
* @deprecated This import will be removed in [email protected].
*
* 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 [email protected].
*
Expand Down
4 changes: 3 additions & 1 deletion packages/nuqs/src/adapters/react-router/v6.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const {
useSearchParams
)

export { enableHistorySync, useOptimisticSearchParams }
export { useOptimisticSearchParams }

export const NuqsAdapter = createAdapterProvider(useNuqsReactRouterV6Adapter)

enableHistorySync()
4 changes: 3 additions & 1 deletion packages/nuqs/src/adapters/react-router/v7.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const {
useSearchParams
)

export { enableHistorySync, useOptimisticSearchParams }
export { useOptimisticSearchParams }

export const NuqsAdapter = createAdapterProvider(useNuqsReactRouterV7Adapter)

enableHistorySync()
4 changes: 3 additions & 1 deletion packages/nuqs/src/adapters/remix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const {
useOptimisticSearchParams
} = createReactRouterBasedAdapter('remix', useNavigate, useSearchParams)

export { enableHistorySync, useOptimisticSearchParams }
export { useOptimisticSearchParams }

export const NuqsAdapter = createAdapterProvider(useNuqsRemixAdapter)

enableHistorySync()
14 changes: 4 additions & 10 deletions packages/nuqs/src/useQueryState.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -260,7 +254,7 @@ export function useQueryState<T = string>(
}, [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
Expand Down Expand Up @@ -288,7 +282,7 @@ export function useQueryState<T = string>(
) {
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,
Expand All @@ -297,7 +291,7 @@ export function useQueryState<T = string>(
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]
Expand Down
19 changes: 4 additions & 15 deletions packages/nuqs/src/useQueryStates.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -139,7 +132,7 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
])

// Sync all hooks together & with external URL changes
useInsertionEffect(() => {
useEffect(() => {
function updateInternalState(state: V) {
debug('[nuq+ `%s`] updateInternalState %O', stateKeys, state)
stateRef.current = state
Expand Down Expand Up @@ -216,8 +209,7 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
) {
value = null
}

queryRef.current[urlKey] = enqueueQueryStringUpdate(
const query = enqueueQueryStringUpdate(
urlKey,
value,
parser.serialize ?? String,
Expand All @@ -235,10 +227,7 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
startTransition
}
)
emitter.emit(urlKey, {
state: value,
query: queryRef.current[urlKey] ?? null
})
emitter.emit(urlKey, { state: value, query })
}
return scheduleFlushToURL(adapter)
},
Expand Down

0 comments on commit 9ceda53

Please sign in to comment.