react router: `history: 'push'` write with `shallow: false` does not advance the history index
- Lenguaje dominante
- TypeScript
- Estrellas
- 10.8k
- Forks
- 294
- Merge medio
- 1 d 16 h
- PR fusionados (30 d)
- 21
Descripción
## Context
What's your version of `nuqs`?
```
"nuqs": "^2.9.6",
```
What framework are you using?
- ❌ Next.js (app router)
- ❌ Next.js (pages router)
- ❌ React SPA (no router)
- ❌ Remix
- ✅ React Router
- ❌ Other (please specify)
Which version of your framework are you using?
```
"react": "19.0.0",
"react-dom": "19.0.0",
"react-router": "^6.30.2",
"react-router-dom": "^6.30.2",
```
## Description
(This is my first time posting here, and I am not sure about your AI disclosure policy. I used Opus to help me prepare the reproduction and the description below. I have verified the bug manually before submission.)
A `useQueryState` write with `{ history: 'push', shallow: false }` adds a browser history
entry and then tells react-router it was a replace.
[`packages/nuqs/src/adapters/lib/react-router.ts#L66-L89`](https://github.com/47ng/nuqs/blob/aff877ca959e3c4484718965e3d1a0d20542a611/packages/nuqs/src/adapters/lib/react-router.ts#L66-L89):
```ts
const updateMethod =
options.history === 'push' ? history.pushState : history.replaceState
setQueueResetMutex(options.shallow ? 1 : 2)
updateMethod.call(
history,
history.state, // Maintain the history state
historyUpdateMarker,
url
)
let navigationSettled: Promise | undefined
if (options.shallow === false) {
const maybePromise = navigate(
{
// Somehow passing the full URL object here strips the search params
// when accessing the request.url in loaders.
hash: url.hash,
search: url.search
},
{
replace: true,
preventScrollReset: true,
state: history.state?.usr
}
)
```
`updateMethod.call` (L69) really pushes; `navigate(..., { replace: true })` (L77-L85) then
tells the router nothing was pushed. Both cannot be true, and the router acts on the
second. Passing `history.state` through (L71) also gives the new entry the `idx` of the
entry below it — react-router's own index, which it writes on every push and reads back to
size a traversal.
[`packages/router/history.ts#L623-L645`](https://github.com/remix-run/react-router/blob/72973b6493d27014aa76a23a95e3ca186616c4fd/packages/router/history.ts#L623-L645)
(`@remix-run/router@1.23.3`, the version `react-router-dom@6.30.x` depends on):
```ts
function getIndex(): number {
let state = globalHistory.state || { idx: null };
return state.idx;
}
function handlePop() {
action = Action.Pop;
let nextIndex = getIndex();
let delta = nextIndex == null ? null : nextIndex - index; // 0 after a nuqs push
...
}
function push(to: To, state?: any) {
...
index = getIndex() + 1; // what nuqs skips
```
After one such write the router's index is permanently one behind the browser stack, and a
genuine back reports `delta: 0` instead of `-1`. react-router warns about exactly this
desync — "navigating outside the router via `window.history.pushState`" — in
[`packages/router/router.ts#L1038-L1046`](https://github.com/remix-run/react-router/blob/72973b6493d27014aa76a23a95e3ca186616c4fd/packages/router/router.ts#L1038-L1046),
except here the adapter is what navigated.
**Expected:** the router's index still matches the browser stack, as it does after
`navigate(to)`. Either let react-router perform the push
(`navigate(..., { replace: false })`) or write a state with `idx + 1`. The reproduction's
control step shows `navigate()` producing `idx: 1` where the nuqs write produces `idx: 0`.
`shallow: true` is not a workaround: react-router is then never told at all and its
location goes stale. Neither setting leaves the router consistent with the browser.
### Downstream effect on Ionic
`@ionic/react-router` builds its own `LocationHistory` from react-router's action stream.
Told "replace", it drops the entry for the current page (`LocationHistory._replace` pops,
then adds) while both entries still exist in the browser. The next back therefore arrives
at a pathname it does not expect, and it force-navigates to what it believes came before.
My app keeps its open-modal stack in a query parameter, pushed so that back closes the
modal. Closing one instead sends the user a page backwards, in some flows several pages.
Measured across a single modal open: `history.length` 16 → 17, `history.state.idx`
unchanged at 10. Ionic's recovery is aggressive, but it is reacting to being told a push
was a replace.
## Reproduction
Example: Steps to reproduce the behavior:
```sh
git clone -b nuqs-push-history-idx https://github.com/ptmkenny/ionic-react-router-6-test
cd ionic-react-router-6-test
npm install
npm run dev
```
Open and click the four buttons in order. Each click appends a row
to the table.
Guía de contribución
Evaluación
Este issue todavía no se ha evaluado.