#1531·nuqs

Object-returning parsers lose referential equality for values parsed from the URL, causing render loops

Author: GjdoalfnrxuCreated Aug 6, 2026Updated Aug 7, 2026

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