Skip to content

Commit

Permalink
fix: Don't rely on URL for initial state
Browse files Browse the repository at this point in the history
This makes the first render on client side navigation
use the source URL rather than the destination,
which can cause issues if the first state is memoised
or used for some other purposes.

useSearchParams contains the destination values for searchParams,
however this might break in older versions of Next.js (TBC by CI).
In which case this fix might only land on v2.
  • Loading branch information
franky47 committed Apr 21, 2024
1 parent 0de6b92 commit 9912eef
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 14 deletions.
13 changes: 13 additions & 0 deletions packages/e2e/cypress/e2e/repro-542.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference types="cypress" />

it('Reproduction for issue #542', () => {
cy.visit('/app/repro-542/a?q=foo&r=bar')
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
cy.get('#q').should('have.text', 'foo')
cy.get('#r').should('have.text', 'bar')
cy.get('a').click()
cy.location('search').should('eq', '')
cy.get('#q').should('have.text', '')
cy.get('#r').should('have.text', '')
cy.get('#initial').should('have.text', '{"q":null,"r":null}')
})
29 changes: 29 additions & 0 deletions packages/e2e/src/app/app/repro-542/a/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client'

import Link from 'next/link'
import { parseAsString, useQueryState, useQueryStates } from 'nuqs'
import { Suspense } from 'react'

export default function Page() {
return (
<Suspense>
<Client />
</Suspense>
)
}

function Client() {
console.log(
'rendering page a, url: %s',
typeof location !== 'undefined' ? location.href : 'ssr'
)
const [q] = useQueryState('q')
const [{ r }] = useQueryStates({ r: parseAsString })
return (
<>
<div id="q">{q}</div>
<div id="r">{r}</div>
<Link href="/app/repro-542/b">Go to page B</Link>
</>
)
}
32 changes: 32 additions & 0 deletions packages/e2e/src/app/app/repro-542/b/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client'

import { parseAsString, useQueryState, useQueryStates } from 'nuqs'
import React, { Suspense } from 'react'

export default function Page() {
return (
<Suspense>
<Client />
</Suspense>
)
}

function Client() {
console.log(
'rendering page b, url: %s',
typeof location !== 'undefined' ? location.href : 'ssr'
)
const ref = React.useRef<any>(null)
const [q] = useQueryState('q')
const [{ r }] = useQueryStates({ r: parseAsString })
if (ref.current === null) {
ref.current = { q, r }
}
return (
<>
<div id="q">{q}</div>
<div id="r">{r}</div>
<div id="initial">{JSON.stringify(ref.current)}</div>
</>
)
}
7 changes: 1 addition & 6 deletions packages/nuqs/src/useQueryState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,7 @@ export function useQueryState<T = string>(
const initialSearchParams = useSearchParams()
const [internalState, setInternalState] = React.useState<T | null>(() => {
const queueValue = getQueuedValue(key)
const urlValue =
typeof location !== 'object'
? // SSR
initialSearchParams?.get(key) ?? null
: // Components mounted after page load must use the current URL value
new URLSearchParams(location.search).get(key) ?? null
const urlValue = initialSearchParams?.get(key) ?? null
const value = queueValue ?? urlValue
return value === null ? null : safeParse(parse, value, key)
})
Expand Down
11 changes: 3 additions & 8 deletions packages/nuqs/src/useQueryStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,9 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
const router = useRouter()
// Not reactive, but available on the server and on page load
const initialSearchParams = useSearchParams()
const [internalState, setInternalState] = React.useState<V>(() => {
if (typeof location !== 'object') {
// SSR
return parseMap(keyMap, initialSearchParams ?? new URLSearchParams())
}
// Components mounted after page load must use the current URL value
return parseMap(keyMap, new URLSearchParams(location.search))
})
const [internalState, setInternalState] = React.useState<V>(() =>
parseMap(keyMap, initialSearchParams ?? new URLSearchParams())
)
const stateRef = React.useRef(internalState)
debug(
'[nuq+ `%s`] render - state: %O, iSP: %s',
Expand Down

0 comments on commit 9912eef

Please sign in to comment.