Skip to content

Commit

Permalink
fix: Regression in mounting components (#461)
Browse files Browse the repository at this point in the history
* test: Add failing test for repro 359 (regression)

* test: Attempt to de-flake render flashes

* fix: Don't sync against reactive useSearchParams

Since applying the search params to the URL is deferred,
it causes the 359 regression: on mount, the component
with a useQueryState identical to the one that triggered
its mounting would have a correct internal state, but that
would get overriden by a stale (not yet updated) iSP.

Trying to remove the whole sync logic to see how
previous versions may break, and adding 14.0.5-canary.54
which stabilises WHS for future-proofing until GA.

* fix: Restore sync for 14.0.3 (broken patched history)

We'll release a patch fix for the 1.x release line and this
is going straight to the bin in v2, it will only support 14.0.4 onwards.

* chore: Add comment about 14.0.3 hack
  • Loading branch information
franky47 authored Jan 17, 2024
1 parent 62fb943 commit 6b6d190
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
- '14.0.1'
# 14.0.2 is not compatible due to a prefetch issue
- latest
- '14.0.5-canary.54' # WHS stabilised
include:
# 14.0.3 requires the WHS flag
- next-version: '14.0.3'
Expand Down
52 changes: 52 additions & 0 deletions packages/e2e/cypress/e2e/repro-359.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// <reference types="cypress" />

it.only('repro-359', () => {
cy.visit('/app/repro-359')
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')

cy.location('search').should('eq', '')
cy.get('#nuqs-param').should('have.text', 'null')
cy.get('#nuqs-component').should('have.text', '')
cy.get('#nuqss-param').should('have.text', 'null')
cy.get('#nuqss-component').should('have.text', '')

cy.contains('Component 1 (nuqs)').click()
cy.wait(100)
cy.location('search').should('eq', '?param=comp1&component=comp1')
cy.get('#comp1').should('have.text', 'comp1')
cy.get('#comp2').should('not.exist')
cy.get('#nuqs-param').should('have.text', 'comp1')
cy.get('#nuqs-component').should('have.text', 'comp1')
cy.get('#nuqss-param').should('have.text', 'comp1')
cy.get('#nuqss-component').should('have.text', 'comp1')

cy.contains('Component 2 (nuqs)').click()
cy.wait(100)
cy.location('search').should('eq', '?param=comp2&component=comp2')
cy.get('#comp1').should('not.exist')
cy.get('#comp2').should('have.text', 'comp2')
cy.get('#nuqs-param').should('have.text', 'comp2')
cy.get('#nuqs-component').should('have.text', 'comp2')
cy.get('#nuqss-param').should('have.text', 'comp2')
cy.get('#nuqss-component').should('have.text', 'comp2')

cy.contains('Component 1 (nuq+)').click()
cy.wait(100)
cy.location('search').should('eq', '?param=comp1&component=comp1')
cy.get('#comp1').should('have.text', 'comp1')
cy.get('#comp2').should('not.exist')
cy.get('#nuqs-param').should('have.text', 'comp1')
cy.get('#nuqs-component').should('have.text', 'comp1')
cy.get('#nuqss-param').should('have.text', 'comp1')
cy.get('#nuqss-component').should('have.text', 'comp1')

cy.contains('Component 2 (nuq+)').click()
cy.wait(100)
cy.location('search').should('eq', '?param=comp2&component=comp2')
cy.get('#comp1').should('not.exist')
cy.get('#comp2').should('have.text', 'comp2')
cy.get('#nuqs-param').should('have.text', 'comp2')
cy.get('#nuqs-component').should('have.text', 'comp2')
cy.get('#nuqss-param').should('have.text', 'comp2')
cy.get('#nuqss-component').should('have.text', 'comp2')
})
86 changes: 86 additions & 0 deletions packages/e2e/src/app/app/repro-359/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// https://github.com/47ng/nuqs/issues/359

'use client'

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

const paramParser = parseAsString.withDefault('null')
const components = ['comp1', 'comp2'] as const
const componentParser = parseAsStringLiteral(components)

const Component = (props: React.ComponentProps<'span'>) => {
const [param] = useQueryState('param', paramParser)
return <span {...props}>{param}</span>
}

export default function Wolf359() {
const [param, setParam] = useQueryState('param', paramParser)
const [component, seComponent] = useQueryState('component', componentParser)
const [multiple, setMultiple] = useQueryStates({
param: paramParser,
component: componentParser
})
return (
<>
<div>
{component === 'comp1' && <Component id="comp1" />}
{component === 'comp2' && <Component id="comp2" />}
</div>
<div>
<span id="nuqs-param">{param}</span>
<span id="nuqs-component">{component}</span>
<span id="nuqss-param">{multiple.param}</span>
<span id="nuqss-component">{multiple.component}</span>
</div>
<div>
<button
onClick={() => {
setParam('comp1')
seComponent('comp1')
}}
>
Component 1 (nuqs)
</button>
<button
onClick={() => {
setParam('comp2')
seComponent('comp2')
}}
>
Component 2 (nuqs)
</button>
<br />
<button
onClick={() => {
setMultiple({
param: 'comp1',
component: 'comp1'
})
}}
>
Component 1 (nuq+)
</button>
<button
onClick={() => {
setMultiple({
param: 'comp2',
component: 'comp2'
})
}}
>
Component 2 (nuq+)
</button>
</div>
<nav>
<Link href="?param=comp1&component=comp1">Comp 1</Link>
<Link href="?param=comp2&component=comp2">Comp 2</Link>
</nav>
</>
)
}
1 change: 1 addition & 0 deletions packages/nuqs/src/update-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export function scheduleFlushToURL(router: Router) {
declare global {
interface Window {
next: {
version: string
router?: NextRouter & {
state: {
asPath: string
Expand Down
5 changes: 5 additions & 0 deletions packages/nuqs/src/useQueryState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@ export function useQueryState<T = string>(
)

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 value = initialSearchParams.get(key) ?? null
const state = value === null ? null : safeParse(parse, value, key)
debug('[nuqs `%s`] syncFromUseSearchParams %O', key, state)
Expand Down

0 comments on commit 6b6d190

Please sign in to comment.