Skip to content

Commit

Permalink
fix: Cache the query string in useQueryStates (#631)
Browse files Browse the repository at this point in the history
The cache key for testing if the query changed 
wasn't being updated in useQueryStates.

This also revealed it being broken in next@14.0.3, 
where the history patching mechanism wasn't working. 
A fix has been added and will be removed in v2 
(where support for those version ranges is dropped).
  • Loading branch information
franky47 authored Sep 5, 2024
1 parent e65560b commit 9dc148f
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 0 deletions.
39 changes: 39 additions & 0 deletions packages/e2e/cypress/e2e/repro-630.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/// <reference types="cypress" />

describe('Reproduction for issue #630', () => {
it('works with useQueryState', () => {
runTest('1')
})
it('works with useQueryStates', () => {
runTest('3')
})
})

function runTest(sectionToTry) {
cy.visit('/app/repro-630')
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
cy.get('#1-pre').should('have.text', '{"a":null,"b":null}')
cy.get('#2-pre').should('have.text', '{"a":null,"b":null}')
cy.get('#3-pre').should('have.text', '{"a":null,"b":null}')
cy.get('#4-pre').should('have.text', '{"a":null,"b":null}')
cy.get(`#${sectionToTry}-set`).click()
cy.get('#1-pre').should('have.text', '{"a":"1","b":"2"}')
cy.get('#2-pre').should('have.text', '{"a":"1","b":"2"}')
cy.get('#3-pre').should('have.text', '{"a":"1","b":"2"}')
cy.get('#4-pre').should('have.text', '{"a":"1","b":"2"}')
cy.get(`#${sectionToTry}-clear`).click()
cy.get('#1-pre').should('have.text', '{"a":null,"b":null}')
cy.get('#2-pre').should('have.text', '{"a":null,"b":null}')
cy.get('#3-pre').should('have.text', '{"a":null,"b":null}')
cy.get('#4-pre').should('have.text', '{"a":null,"b":null}')
cy.go('back')
cy.get('#1-pre').should('have.text', '{"a":"1","b":"2"}')
cy.get('#2-pre').should('have.text', '{"a":"1","b":"2"}')
cy.get('#3-pre').should('have.text', '{"a":"1","b":"2"}')
cy.get('#4-pre').should('have.text', '{"a":"1","b":"2"}')
cy.go('back')
cy.get('#1-pre').should('have.text', '{"a":null,"b":null}')
cy.get('#2-pre').should('have.text', '{"a":null,"b":null}')
cy.get('#3-pre').should('have.text', '{"a":null,"b":null}')
cy.get('#4-pre').should('have.text', '{"a":null,"b":null}')
}
79 changes: 79 additions & 0 deletions packages/e2e/src/app/app/repro-630/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use client'

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

export default function Page() {
return (
<Suspense>
<Client id="1" />
<Client id="2" />
<Clients id="3" />
<Clients id="4" />
</Suspense>
)
}

type ClientProps = {
id: string
}

function Client({ id }: ClientProps) {
const [a, setA] = useQueryState(
'a',
parseAsString.withOptions({ history: 'push' })
)
const [b, setB] = useQueryState(
'b',
parseAsString.withOptions({ history: 'push' })
)

return (
<>
<p>useQueryState {id}</p>
<pre id={`${id}-pre`}>{JSON.stringify({ a, b })}</pre>
<button
id={`${id}-set`}
onClick={() => {
setA('1')
setB('2')
}}
>
Set
</button>
<button
id={`${id}-clear`}
onClick={() => {
setA(null)
setB(null)
}}
>
Clear
</button>
<hr />
</>
)
}

function Clients({ id }: ClientProps) {
const [params, setParams] = useQueryStates(
{
a: parseAsString,
b: parseAsString
},
{ history: 'push' }
)
return (
<>
<p>useQueryStates {id}</p>
<pre id={`${id}-pre`}>{JSON.stringify(params)}</pre>
<button id={`${id}-set`} onClick={() => setParams({ a: '1', b: '2' })}>
Set
</button>
<button id={`${id}-clear`} onClick={() => setParams(null)}>
Clear
</button>
<hr />
</>
)
}
23 changes: 23 additions & 0 deletions packages/nuqs/src/useQueryStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,26 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
initialSearchParams
)

React.useEffect(() => {
// This will be removed in v2 which will drop support for
// partially-functional shallow routing (14.0.2 and 14.0.3)
if (window.next?.version !== '14.0.3') {
return
}
const state = parseMap(
keyMap,
initialSearchParams,
queryRef.current,
stateRef.current
)
setInternalState(state)
}, [
Object.keys(keyMap)
.map(key => initialSearchParams?.get(key))
.join('&'),
keys
])

// Sync all hooks together & with external URL changes
React.useInsertionEffect(() => {
function updateInternalState(state: V) {
Expand Down Expand Up @@ -216,6 +236,9 @@ function parseMap<KeyMap extends UseQueryStatesKeysMap>(
}
const value = query === null ? null : safeParse(parse, query, key)
obj[key as keyof KeyMap] = value ?? defaultValue ?? null
if (cachedQuery) {
cachedQuery[key] = query
}
return obj
}, {} as Values<KeyMap>)
}

0 comments on commit 9dc148f

Please sign in to comment.