47ng/nuqs

bug: stale state if keys are the same

Closed this issue · 10 comments

Context

What's your version of next-usequerystate?

"next-usequerystate": "^1.13.2"

Next.js information (obtained by running next info):

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 22.5.0: Mon Apr 24 20:52:24 PDT 2023; root:xnu-8796.121.2~5/RELEASE_ARM64_T6000
Binaries:
  Node: 18.18.2
  npm: 9.8.1
  Yarn: 1.22.19
  pnpm: 8.6.0
Relevant Packages:
  next: 14.0.3
  eslint-config-next: 14.0.3
  react: 18.0.0
  react-dom: 18.0.0
  typescript: 5.0.2
Next.js Config:
  output: N/A

Are you using:

  • ✅ The app router

Description

So, I have multiple pages in my dashboard, all of them have data table which uses these query state

  • page
  • limit
  • search

The problem is that, when i search in Product Page and navigate to Client Page it populate the search state in Client Page
with the state from Product Page but there is no such query in the url.

Product Page: url?search=sth -> navigate to other page -> Other Page: url // doesnt have query but search state is "sth"

Reproduction

https://codesandbox.io/p/devbox/next-querystate-bug-report-vts5r5

  1. write something into the input which update search query state
  2. click on go to another page
  3. notice that the url doesnt have search query anymore which is correct
  4. but the state is inherited from the Home page as u can see if the text display on top of the input

Check out the known issues / compatibility table.

You'll need to update to Next.js 14.0.4 or enable the experimental windowHistorySupport flag.

I forked your sandbox to update the dependencies, but the problem still occurs on CodeSandbox (inconsistently though). However, I also copied your example code to the playground, and it works fine there using Next.js 14.0.4 (in both dev and prod, with or without Turbopack). See #431.

My guess is that since CodeSandbox is kind of a browser-in-a-browser, they're probably doing hacky things with the history API which may explain why some state gets stuck between pages. If that's the case I'll probably stop recommending using it for reproductions, and instead open PRs against the playground which get automatically deployed on Vercel.

I will try ur solution in my project and update this issue accordingly, Thanks!

I have tried your solution but it didnt solve the problem completely.

  • next@v14.0.4
  • windowHistorySupport

And it is as you have said the problem isnt consistent anymore.

  1. ?perPage=10 ✅
    It fixed this queryState, which mean navigation between pages doesnt inherit perPage from another page.

  2. ?date=2023-11-30T17:00:00.000Z,2023-12-19T03:39:54.185Z ❌
    Although the problem still occur in this queryState, when navigating to another page it inherit the state.

Are you trying on CodeSandbox or in a local dev environment ?

If you're on Next.js 14.0.4, the windowHistorySupport flag is not necessary (it was needed only on 14.0.3), though it is supported.

Could you try and enable debug logs and paste them here please ?

Im not sure if this is wut u mean when u say paste the log here😅, this include me selecting a date range and then navigate to another page.

oh and also its on local dev env not CodeSandbox

[nuqs] Patching history with 1.13.2
chunk-RKKJ3LQ3.js:27 [nuqs] history.replaceState(/expenses) (external) Object
chunk-RKKJ3LQ3.js:27 [nuqs] External history.replaceState call: triggering sync with 
page.tsx:40 [nuqs `date`] render - state: null, iSP: null
page.tsx:42 [nuqs `searchExpense`] render - state: null, iSP: null
page.tsx:49 [nuqs `page`] render - state: null, iSP: null
page.tsx:56 [nuqs `perPage`] render - state: null, iSP: null
installHook.js:1 [nuqs `date`] render - state: null, iSP: null
installHook.js:1 [nuqs `searchExpense`] render - state: null, iSP: null
installHook.js:1 [nuqs `page`] render - state: null, iSP: null
installHook.js:1 [nuqs `perPage`] render - state: null, iSP: null
chunk-RKKJ3LQ3.js:27 [nuqs `date`] subscribing to sync
chunk-RKKJ3LQ3.js:27 [nuqs `searchExpense`] subscribing to sync
chunk-RKKJ3LQ3.js:27 [nuqs `page`] subscribing to sync
chunk-RKKJ3LQ3.js:27 [nuqs `perPage`] subscribing to sync
chunk-RKKJ3LQ3.js:27 [nuqs `date`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `searchExpense`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `page`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `perPage`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `date`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `searchExpense`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `page`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `perPage`] syncFromUseSearchParams null
page.tsx:40 [nuqs `date`] render - state: null, iSP: null
page.tsx:42 [nuqs `searchExpense`] render - state: null, iSP: null
page.tsx:49 [nuqs `page`] render - state: null, iSP: null
page.tsx:56 [nuqs `perPage`] render - state: null, iSP: null
installHook.js:1 [nuqs `date`] render - state: null, iSP: null
installHook.js:1 [nuqs `searchExpense`] render - state: null, iSP: null
installHook.js:1 [nuqs `page`] render - state: null, iSP: null
installHook.js:1 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:40 [nuqs `date`] render - state: null, iSP: null
page.tsx:42 [nuqs `searchExpense`] render - state: null, iSP: null
page.tsx:49 [nuqs `page`] render - state: null, iSP: null
page.tsx:56 [nuqs `perPage`] render - state: null, iSP: null
installHook.js:1 [nuqs `date`] render - state: null, iSP: null
installHook.js:1 [nuqs `searchExpense`] render - state: null, iSP: null
installHook.js:1 [nuqs `page`] render - state: null, iSP: null
installHook.js:1 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:40 [nuqs `date`] render - state: null, iSP: null
page.tsx:42 [nuqs `searchExpense`] render - state: null, iSP: null
page.tsx:49 [nuqs `page`] render - state: null, iSP: null
page.tsx:56 [nuqs `perPage`] render - state: null, iSP: null
installHook.js:1 [nuqs `date`] render - state: null, iSP: null
installHook.js:1 [nuqs `searchExpense`] render - state: null, iSP: null
installHook.js:1 [nuqs `page`] render - state: null, iSP: null
installHook.js:1 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:82 [nuqs `page`] updateInternalState 1
page.tsx:82 [nuqs queue] Enqueueing page=1 Object
page.tsx:83 [nuqs `date`] updateInternalState Object
page.tsx:83 [nuqs queue] Enqueueing date=2023-11-30T17:00:00.000Z,2023-12-19T08:59:32.264Z Object
page.tsx:40 [nuqs `date`] render - state: Object, iSP: null
page.tsx:42 [nuqs `searchExpense`] render - state: null, iSP: null
page.tsx:49 [nuqs `page`] render - state: 1, iSP: null
page.tsx:56 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:40 [nuqs `date`] render - state: Object, iSP: null
page.tsx:42 [nuqs `searchExpense`] render - state: null, iSP: null
page.tsx:49 [nuqs `page`] render - state: 1, iSP: null
page.tsx:56 [nuqs `perPage`] render - state: null, iSP: null
chunk-RKKJ3LQ3.js:27 [nuqs queue] Scheduling flush in 0 ms. Throttled at 50 ms
chunk-RKKJ3LQ3.js:27 [nuqs queue] Flushing queue Array(2) with options Object
chunk-RKKJ3LQ3.js:27 [nuqs queue (app)] Updating url: http://localhost:3000/expenses?page=1&date=2023-11-30T17:00:00.000Z,2023-12-19T08:59:32.264Z
chunk-RKKJ3LQ3.js:27 [nuqs] history.replaceState(http://localhost:3000/expenses?page=1&date=2023-11-30T17:00:00.000Z,2023-12-19T08:59:32.264Z) (internal) Object
page.tsx:41 [nuqs `date`] render - state: Object, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: 1, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: Object, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: 1, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
chunk-RKKJ3LQ3.js:27 [nuqs] history.pushState(/payments) (external) Object
chunk-RKKJ3LQ3.js:27 [nuqs `date`] unsubscribing from sync
chunk-RKKJ3LQ3.js:27 [nuqs `searchExpense`] unsubscribing from sync
chunk-RKKJ3LQ3.js:27 [nuqs `page`] unsubscribing from sync
chunk-RKKJ3LQ3.js:27 [nuqs `perPage`] unsubscribing from sync
chunk-RKKJ3LQ3.js:27 [nuqs `date`] subscribing to sync
chunk-RKKJ3LQ3.js:27 [nuqs `searchPayment`] subscribing to sync
chunk-RKKJ3LQ3.js:27 [nuqs `page`] subscribing to sync
chunk-RKKJ3LQ3.js:27 [nuqs `perPage`] subscribing to sync
chunk-RKKJ3LQ3.js:27 [nuqs `date`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `searchPayment`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `page`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `perPage`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `date`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `searchPayment`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `page`] syncFromUseSearchParams null
chunk-RKKJ3LQ3.js:27 [nuqs `perPage`] syncFromUseSearchParams null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
chunk-RKKJ3LQ3.js:27 [nuqs] External history.pushState call: triggering sync with 
chunk-RKKJ3LQ3.js:27 [nuqs `date`] syncFromURL null
chunk-RKKJ3LQ3.js:27 [nuqs `date`] updateInternalState null
chunk-RKKJ3LQ3.js:27 [nuqs `searchPayment`] syncFromURL null
chunk-RKKJ3LQ3.js:27 [nuqs `searchPayment`] updateInternalState null
chunk-RKKJ3LQ3.js:27 [nuqs `page`] syncFromURL null
chunk-RKKJ3LQ3.js:27 [nuqs `page`] updateInternalState null
chunk-RKKJ3LQ3.js:27 [nuqs `perPage`] syncFromURL null
chunk-RKKJ3LQ3.js:27 [nuqs `perPage`] updateInternalState null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
page.tsx:41 [nuqs `date`] render - state: null, iSP: null
page.tsx:43 [nuqs `searchPayment`] render - state: null, iSP: null
page.tsx:50 [nuqs `page`] render - state: null, iSP: null
page.tsx:57 [nuqs `perPage`] render - state: null, iSP: null
```

The logs show the date key being correctly reset after navigation. Could this be a default value being returned?

Would you mind sharing your hook declarations?

This is how i currently use it.

const [dateParam, setDateParam] = useQueryState("date", parseAsDateRange)

export const parseAsDateRange = createParser({
  parse: (dateRangeString: string) => {
    if (!dateRangeString) return null
    
    if (typeof dateRangeString !== "string") return null
    
    const [from, to] = dateRangeString.split(",")
    
    const dateRangeSchema = z.object({
      from: z.date(),
      to: z.date(),
    })
    
    const validatedDateRange = dateRangeSchema.safeParse({
      from: new Date(from),
      to: new Date(to),
    })
    
    return validatedDateRange.success
      ? (validatedDateRange.data as DateRange)
      : null
  },
  serialize: (dateRange) => {
    if (!dateRange) return ""
    
    if (typeof dateRange !== "object") return ""
    
    const { from, to } = dateRange
    
    if (!from) return ""
    
    return `${from?.toISOString()},${to?.toISOString()}`
  },
}).withOptions({ history: "replace" })

I added your custom parser (which looks fine) to the reproduction in the playground, and it works as expected:

https://next-usequerystate-f99tui9r1-47ng.vercel.app/demos/repro-430

Hey there! It's strange, but it seems like the problem has magically disappeared, so I'll go ahead and close this issue now. Oh, and I just wanted to say a big thank you for all your help. It's been truly appreciated!