Skip to content

Commit

Permalink
fix: Read queued updates when initialising state (#703)
Browse files Browse the repository at this point in the history
* fix: Read queued updates when initialising state

Closes #702.

* test: Add non-regression test
  • Loading branch information
franky47 authored Oct 24, 2024
1 parent e4addb4 commit caa3779
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 2 deletions.
24 changes: 24 additions & 0 deletions packages/e2e/cypress/e2e/repro-702.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// <reference types="cypress" />

describe('repro-702', () => {
it('mounts components with the correct state on load', () => {
cy.visit('/app/repro-702?a=test&b=test')
cy.get('#conditional-a-useQueryState').should('have.text', 'test pass')
cy.get('#conditional-a-useQueryStates').should('have.text', 'test pass')
cy.get('#conditional-b-useQueryState').should('have.text', 'test pass')
cy.get('#conditional-b-useQueryStates').should('have.text', 'test pass')
})

it('mounts components with the correct state after an update', () => {
cy.visit('/app/repro-702')
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')

cy.get('#trigger-a').click()
cy.get('#conditional-a-useQueryState').should('have.text', 'test pass')
cy.get('#conditional-a-useQueryStates').should('have.text', 'test pass')

cy.get('#trigger-b').click()
cy.get('#conditional-b-useQueryState').should('have.text', 'test pass')
cy.get('#conditional-b-useQueryStates').should('have.text', 'test pass')
})
})
110 changes: 110 additions & 0 deletions packages/e2e/src/app/app/repro-702/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use client'

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

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

function Client() {
return (
<>
<TriggerA />
<TriggerB />
<SwitchComponentA />
<SwitchComponentB />
</>
)
}

// using useQueryState
function TriggerA() {
const [, setState] = useQueryState('a')
return (
<button id="trigger-a" onClick={() => setState('test')}>
Trigger A (via useQueryState)
</button>
)
}

// using useQueryStates
function TriggerB() {
const [, setState] = useQueryStates({
b: parseAsString
})
return (
<button id="trigger-b" onClick={() => setState({ b: 'test' })}>
Trigger B (via useQueryStates)
</button>
)
}

function SwitchComponentA() {
const [x] = useQueryState('a')
if (x === 'test') {
return <ConditionalComponentA />
}
return null
}

function SwitchComponentB() {
const [x] = useQueryState('b')
if (x === 'test') {
return <ConditionalComponentB />
}
return null
}

function ConditionalComponentA() {
const nullCheckUseQueryState = useRef(false)
const nullCheckUseQueryStates = useRef(false)
const [fromUseQueryState] = useQueryState('a')
const [{ a: fromUseQueryStates }] = useQueryStates({ a: parseAsString })

if (fromUseQueryState === null) {
nullCheckUseQueryState.current = true
}
if (fromUseQueryStates === null) {
nullCheckUseQueryStates.current = true
}

return (
<>
<div id="conditional-a-useQueryState">
{fromUseQueryState} {nullCheckUseQueryState.current ? 'fail' : 'pass'}
</div>
<div id="conditional-a-useQueryStates">
{fromUseQueryStates} {nullCheckUseQueryStates.current ? 'fail' : 'pass'}
</div>
</>
)
}

function ConditionalComponentB() {
const nullCheckUseQueryState = useRef(false)
const nullCheckUseQueryStates = useRef(false)
const [fromUseQueryState] = useQueryState('b')
const [{ b: fromUseQueryStates }] = useQueryStates({ b: parseAsString })

if (fromUseQueryState === null) {
nullCheckUseQueryState.current = true
}
if (fromUseQueryStates === null) {
nullCheckUseQueryStates.current = true
}
return (
<>
<div id="conditional-b-useQueryState">
{fromUseQueryState} {nullCheckUseQueryState.current ? 'fail' : 'pass'}
</div>
<div id="conditional-b-useQueryStates">
{fromUseQueryStates} {nullCheckUseQueryStates.current ? 'fail' : 'pass'}
</div>
</>
)
}
4 changes: 4 additions & 0 deletions packages/nuqs/src/update-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const transitionsQueue: Set<React.TransitionStartFunction> = new Set()
let lastFlushTimestamp = 0
let flushPromiseCache: Promise<URLSearchParams> | null = null

export function getQueuedValue(key: string) {
return updateQueue.get(key)
}

export function enqueueQueryStringUpdate<Value>(
key: string,
value: Value | null,
Expand Down
7 changes: 6 additions & 1 deletion packages/nuqs/src/useQueryState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { emitter, type CrossHookSyncPayload } from './sync'
import {
FLUSH_RATE_LIMIT_MS,
enqueueQueryStringUpdate,
getQueuedValue,
scheduleFlushToURL
} from './update-queue'
import { safeParse } from './utils'
Expand Down Expand Up @@ -235,7 +236,11 @@ export function useQueryState<T = string>(
} = useAdapter()
const queryRef = useRef<string | null>(initialSearchParams?.get(key) ?? null)
const [internalState, setInternalState] = useState<T | null>(() => {
const query = initialSearchParams?.get(key) ?? null
const queuedQuery = getQueuedValue(key)
const query =
queuedQuery === undefined
? (initialSearchParams?.get(key) ?? null)
: queuedQuery
return query === null ? null : safeParse(parse, query, key)
})
const stateRef = useRef(internalState)
Expand Down
7 changes: 6 additions & 1 deletion packages/nuqs/src/useQueryStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { emitter, type CrossHookSyncPayload } from './sync'
import {
FLUSH_RATE_LIMIT_MS,
enqueueQueryStringUpdate,
getQueuedValue,
scheduleFlushToURL
} from './update-queue'
import { safeParse } from './utils'
Expand Down Expand Up @@ -258,7 +259,11 @@ function parseMap<KeyMap extends UseQueryStatesKeysMap>(
return Object.keys(keyMap).reduce((obj, stateKey) => {
const urlKey = urlKeys?.[stateKey] ?? stateKey
const { defaultValue, parse } = keyMap[stateKey]!
const query = searchParams?.get(urlKey) ?? null
const queuedQuery = getQueuedValue(urlKey)
const query =
queuedQuery === undefined
? (searchParams?.get(urlKey) ?? null)
: queuedQuery
if (cachedQuery && cachedState && cachedQuery[urlKey] === query) {
obj[stateKey as keyof KeyMap] =
cachedState[stateKey] ?? defaultValue ?? null
Expand Down

0 comments on commit caa3779

Please sign in to comment.