Object-returning parsers lose referential equality for values parsed from the URL, causing render loops
- Ngôn ngữ chính
- TypeScript
- Star
- 10.8k
- Fork
- 294
- Merge trung bình
- 1 ngày 16 giờ
- Pull request đã merge (30 ngày)
- 21
Mô tả
### Summary
A parser whose `parse` returns an object (`Date`, `parseAsJson`) returns a new object identity on nearly every render, even when the query string and the value it encodes are unchanged. Consumers that treat the value as referentially stable, in a `useEffect` dependency array, a `key`, or a memo dep, observe a change that never happened. Where a remount is in the feedback path, the render loop does not settle.
### Environment
- nuqs `2.9.5` (also reproduced on `next` at `8ae4db1`)
- React 19
- Reproduced under both `@testing-library/react` + jsdom and nuqs's own `vitest-browser-react` browser project
### Failing tests
I have added three failing cases to the existing `useQueryState: referential equality` describe block, using your own helpers and browser runner:
https://github.com/Gjdoalfnrxu/nuqs/tree/test/date-parser-referential-equality
```
× should keep referential equality for a Date parsed from the URL
× should keep referential equality for a Date across remounts
× should not re-render indefinitely when a Date is reported upward
```
`pnpm --filter nuqs test:browser` reproduces. The other 25 tests in that file pass.
### Measured behaviour
Two subscribers on the same key, URL never written to, value never changes:
| parser | renders (stays mounted) | distinct identities | renders (re-keyed) | distinct identities |
|---|---|---|---|---|
| `parseAsString` | 4 | 1 | 6 | 1 |
| `parseAsIsoDateTime` | 4 | **2** | 6 | **6** |
Every one of those `Date` objects carries the same instant. A freshly mounted consumer gets a new object on every render; a consumer that merely stays mounted still sees its value swapped once.
### The loop
The third test wires the arrangement this shows up as in real code, with **no writes to the URL anywhere**:
1. a component holds state `S` and re-keys a child whenever `S` changes identity (a menu re-keying its items after a measure)
2. the child reads the param and reports its value upward
3. the report changes `S`'s identity, which re-keys the child, which re-parses, which mints a new object
It throws at 53 renders and is still climbing. Swapping the parser for `parseAsString` settles it in 3.
### Where it comes from, as far as I can tell
`parseMap` caches on the raw query string, and the cache lives in a ref owned by a single hook instance. So a value cached by one hook is invisible to every other instance, which re-parses from scratch, and any newly mounted instance necessarily produces a new object. `compareQuery` compares query strings rather than parsed values, and the emitter sync path compares with `Object.is`, which is why a parser's `eq` does not help here.
Would you mind a PR to add a simple sentence to the docs to make this behaviour clearer (if i didn't just miss it?) in the mean time?
---
I realised after creating these test cases that your existing WIP https://github.com/47ng/nuqs/pull/1482 solves for this failure (i think) so perhaps consider this a vote for upstreaming that, but this behaviour is surprising / a bit of a pain
Hướng dẫn đóng góp
Đánh giá
Issue này chưa được đánh giá.