Skip to content

Commit

Permalink
fix: Referential stability for the state updater function (#841)
Browse files Browse the repository at this point in the history
  • Loading branch information
franky47 authored Jan 3, 2025
1 parent 10e526d commit c089be2
Show file tree
Hide file tree
Showing 24 changed files with 237 additions and 56 deletions.
25 changes: 25 additions & 0 deletions packages/e2e/next/cypress/e2e/shared/referential-stability.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { testReferentialStability } from 'e2e-shared/specs/referential-stability.cy'

testReferentialStability({
path: '/app/referential-stability/useQueryState',
hook: 'useQueryState',
nextJsRouter: 'app'
})

testReferentialStability({
path: '/app/referential-stability/useQueryStates',
hook: 'useQueryStates',
nextJsRouter: 'app'
})

testReferentialStability({
path: '/pages/referential-stability/useQueryState',
hook: 'useQueryState',
nextJsRouter: 'pages'
})

testReferentialStability({
path: '/pages/referential-stability/useQueryStates',
hook: 'useQueryStates',
nextJsRouter: 'pages'
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ReferentialStabilityUseQueryState } from 'e2e-shared/specs/referential-stability'
import { Suspense } from 'react'

export default function Page() {
return (
<Suspense>
<ReferentialStabilityUseQueryState />
</Suspense>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ReferentialStabilityUseQueryStates } from 'e2e-shared/specs/referential-stability'
import { Suspense } from 'react'

export default function Page() {
return (
<Suspense>
<ReferentialStabilityUseQueryStates />
</Suspense>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ReferentialStabilityUseQueryState } from 'e2e-shared/specs/referential-stability'

export default ReferentialStabilityUseQueryState
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ReferentialStabilityUseQueryStates } from 'e2e-shared/specs/referential-stability'

export default ReferentialStabilityUseQueryStates
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { testReferentialStability } from 'e2e-shared/specs/referential-stability.cy'

testReferentialStability({
path: '/referential-stability/useQueryState',
hook: 'useQueryState'
})

testReferentialStability({
path: '/referential-stability/useQueryStates',
hook: 'useQueryStates'
})
38 changes: 20 additions & 18 deletions packages/e2e/react-router/v6/src/react-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,26 @@ function load(mod: Promise<{ default: any; [otherExports: string]: any }>) {
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<RootLayout/>} >,
<Route path='hash-preservation' lazy={load(import('./routes/hash-preservation'))} />
<Route path='basic-io/useQueryState' lazy={load(import('./routes/basic-io.useQueryState'))} />
<Route path='basic-io/useQueryStates' lazy={load(import('./routes/basic-io.useQueryStates'))} />
<Route path='linking/useQueryState' lazy={load(import('./routes/linking.useQueryState'))} />
<Route path='linking/useQueryState/other' lazy={load(import('./routes/linking.useQueryState.other'))} />
<Route path='linking/useQueryStates' lazy={load(import('./routes/linking.useQueryStates'))} />
<Route path='linking/useQueryStates/other' lazy={load(import('./routes/linking.useQueryStates.other'))} />
<Route path='push/useQueryState' lazy={load(import('./routes/push.useQueryState'))} />
<Route path='push/useQueryStates' lazy={load(import('./routes/push.useQueryStates'))} />
<Route path="routing/useQueryState" lazy={load(import('./routes/routing.useQueryState'))} />
<Route path="routing/useQueryState/other" lazy={load(import('./routes/routing.useQueryState.other'))} />
<Route path="routing/useQueryStates" lazy={load(import('./routes/routing.useQueryStates'))} />
<Route path="routing/useQueryStates/other" lazy={load(import('./routes/routing.useQueryStates.other'))} />
<Route path='shallow/useQueryState' lazy={load(import('./routes/shallow.useQueryState'))} />
<Route path='shallow/useQueryStates' lazy={load(import('./routes/shallow.useQueryStates'))} />
<Route path='loader' lazy={load(import('./routes/loader'))} />
<Route path="form/useQueryState" lazy={load(import('./routes/form.useQueryState'))} />
<Route path="form/useQueryStates" lazy={load(import('./routes/form.useQueryStates'))} />
<Route path='hash-preservation' lazy={load(import('./routes/hash-preservation'))} />
<Route path='basic-io/useQueryState' lazy={load(import('./routes/basic-io.useQueryState'))} />
<Route path='basic-io/useQueryStates' lazy={load(import('./routes/basic-io.useQueryStates'))} />
<Route path='linking/useQueryState' lazy={load(import('./routes/linking.useQueryState'))} />
<Route path='linking/useQueryState/other' lazy={load(import('./routes/linking.useQueryState.other'))} />
<Route path='linking/useQueryStates' lazy={load(import('./routes/linking.useQueryStates'))} />
<Route path='linking/useQueryStates/other' lazy={load(import('./routes/linking.useQueryStates.other'))} />
<Route path='push/useQueryState' lazy={load(import('./routes/push.useQueryState'))} />
<Route path='push/useQueryStates' lazy={load(import('./routes/push.useQueryStates'))} />
<Route path="routing/useQueryState" lazy={load(import('./routes/routing.useQueryState'))} />
<Route path="routing/useQueryState/other" lazy={load(import('./routes/routing.useQueryState.other'))} />
<Route path="routing/useQueryStates" lazy={load(import('./routes/routing.useQueryStates'))} />
<Route path="routing/useQueryStates/other" lazy={load(import('./routes/routing.useQueryStates.other'))} />
<Route path='shallow/useQueryState' lazy={load(import('./routes/shallow.useQueryState'))} />
<Route path='shallow/useQueryStates' lazy={load(import('./routes/shallow.useQueryStates'))} />
<Route path='loader' lazy={load(import('./routes/loader'))} />
<Route path="form/useQueryState" lazy={load(import('./routes/form.useQueryState'))} />
<Route path="form/useQueryStates" lazy={load(import('./routes/form.useQueryStates'))} />
<Route path="referential-stability/useQueryState" lazy={load(import('./routes/referential-stability.useQueryState'))} />
<Route path="referential-stability/useQueryStates" lazy={load(import('./routes/referential-stability.useQueryStates'))} />
</Route>
))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ReferentialStabilityUseQueryState } from 'e2e-shared/specs/referential-stability'

export default ReferentialStabilityUseQueryState
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ReferentialStabilityUseQueryStates } from 'e2e-shared/specs/referential-stability'

export default ReferentialStabilityUseQueryStates
38 changes: 20 additions & 18 deletions packages/e2e/react-router/v7/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@ import { type RouteConfig, layout, route } from '@react-router/dev/routes'
export default [
// prettier-ignore
layout('layout.tsx', [
route('/hash-preservation', './routes/hash-preservation.tsx'),
route('/basic-io/useQueryState', './routes/basic-io.useQueryState.tsx'),
route('/basic-io/useQueryStates', './routes/basic-io.useQueryStates.tsx'),
route('/linking/useQueryState', './routes/linking.useQueryState.tsx'),
route('/linking/useQueryState/other', './routes/linking.useQueryState.other.tsx'),
route('/linking/useQueryStates', './routes/linking.useQueryStates.tsx'),
route('/linking/useQueryStates/other', './routes/linking.useQueryStates.other.tsx'),
route('/push/useQueryState', './routes/push.useQueryState.tsx'),
route('/push/useQueryStates', './routes/push.useQueryStates.tsx'),
route('/routing/useQueryState', './routes/routing.useQueryState.tsx'),
route('/routing/useQueryState/other', './routes/routing.useQueryState.other.tsx'),
route('/routing/useQueryStates', './routes/routing.useQueryStates.tsx'),
route('/routing/useQueryStates/other', './routes/routing.useQueryStates.other.tsx'),
route('/shallow/useQueryState', './routes/shallow.useQueryState.tsx'),
route('/shallow/useQueryStates', './routes/shallow.useQueryStates.tsx'),
route('/loader', './routes/loader.tsx'),
route('/form/useQueryState', './routes/form.useQueryState.tsx'),
route('/form/useQueryStates', './routes/form.useQueryStates.tsx'),
route('/hash-preservation', './routes/hash-preservation.tsx'),
route('/basic-io/useQueryState', './routes/basic-io.useQueryState.tsx'),
route('/basic-io/useQueryStates', './routes/basic-io.useQueryStates.tsx'),
route('/linking/useQueryState', './routes/linking.useQueryState.tsx'),
route('/linking/useQueryState/other', './routes/linking.useQueryState.other.tsx'),
route('/linking/useQueryStates', './routes/linking.useQueryStates.tsx'),
route('/linking/useQueryStates/other', './routes/linking.useQueryStates.other.tsx'),
route('/push/useQueryState', './routes/push.useQueryState.tsx'),
route('/push/useQueryStates', './routes/push.useQueryStates.tsx'),
route('/routing/useQueryState', './routes/routing.useQueryState.tsx'),
route('/routing/useQueryState/other', './routes/routing.useQueryState.other.tsx'),
route('/routing/useQueryStates', './routes/routing.useQueryStates.tsx'),
route('/routing/useQueryStates/other', './routes/routing.useQueryStates.other.tsx'),
route('/shallow/useQueryState', './routes/shallow.useQueryState.tsx'),
route('/shallow/useQueryStates', './routes/shallow.useQueryStates.tsx'),
route('/loader', './routes/loader.tsx'),
route('/form/useQueryState', './routes/form.useQueryState.tsx'),
route('/form/useQueryStates', './routes/form.useQueryStates.tsx'),
route('/referential-stability/useQueryState', './routes/referential-stability.useQueryState.tsx'),
route('/referential-stability/useQueryStates', './routes/referential-stability.useQueryStates.tsx')
])
] satisfies RouteConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ReferentialStabilityUseQueryState } from 'e2e-shared/specs/referential-stability'

export default ReferentialStabilityUseQueryState
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ReferentialStabilityUseQueryStates } from 'e2e-shared/specs/referential-stability'

export default ReferentialStabilityUseQueryStates
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { testReferentialStability } from 'e2e-shared/specs/referential-stability.cy'

testReferentialStability({
path: '/referential-stability/useQueryState',
hook: 'useQueryState'
})

testReferentialStability({
path: '/referential-stability/useQueryStates',
hook: 'useQueryStates'
})
11 changes: 11 additions & 0 deletions packages/e2e/react/cypress/e2e/shared/referential-stability.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { testReferentialStability } from 'e2e-shared/specs/referential-stability.cy'

testReferentialStability({
path: '/referential-stability/useQueryState',
hook: 'useQueryState'
})

testReferentialStability({
path: '/referential-stability/useQueryStates',
hook: 'useQueryStates'
})
36 changes: 19 additions & 17 deletions packages/e2e/react/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@ import { JSX, lazy } from 'react'

// prettier-ignore
const routes: Record<string, React.LazyExoticComponent<() => JSX.Element>> = {
'/hash-preservation': lazy(() => import('./routes/hash-preservation')),
'/basic-io/useQueryState': lazy(() => import('./routes/basic-io.useQueryState')),
'/basic-io/useQueryStates': lazy(() => import('./routes/basic-io.useQueryStates')),
'/push/useQueryState': lazy(() => import('./routes/push.useQueryState')),
'/push/useQueryStates': lazy(() => import('./routes/push.useQueryStates')),
'/linking/useQueryState': lazy(() => import('./routes/linking.useQueryState')),
'/linking/useQueryState/other': lazy(() => import('./routes/linking.useQueryState.other')),
'/linking/useQueryStates': lazy(() => import('./routes/linking.useQueryStates')),
'/linking/useQueryStates/other': lazy(() => import('./routes/linking.useQueryStates.other')),
'/routing/useQueryState': lazy(() => import('./routes/routing.useQueryState')),
'/routing/useQueryState/other': lazy(() => import('./routes/routing.useQueryState.other')),
'/routing/useQueryStates': lazy(() => import('./routes/routing.useQueryStates')),
'/routing/useQueryStates/other': lazy(() => import('./routes/routing.useQueryStates.other')),
'/shallow/useQueryState': lazy(() => import('./routes/shallow.useQueryState')),
'/shallow/useQueryStates': lazy(() => import('./routes/shallow.useQueryStates')),
'/form/useQueryState': lazy(() => import('./routes/form.useQueryState')),
'/form/useQueryStates': lazy(() => import('./routes/form.useQueryStates')),
'/hash-preservation': lazy(() => import('./routes/hash-preservation')),
'/basic-io/useQueryState': lazy(() => import('./routes/basic-io.useQueryState')),
'/basic-io/useQueryStates': lazy(() => import('./routes/basic-io.useQueryStates')),
'/push/useQueryState': lazy(() => import('./routes/push.useQueryState')),
'/push/useQueryStates': lazy(() => import('./routes/push.useQueryStates')),
'/linking/useQueryState': lazy(() => import('./routes/linking.useQueryState')),
'/linking/useQueryState/other': lazy(() => import('./routes/linking.useQueryState.other')),
'/linking/useQueryStates': lazy(() => import('./routes/linking.useQueryStates')),
'/linking/useQueryStates/other': lazy(() => import('./routes/linking.useQueryStates.other')),
'/routing/useQueryState': lazy(() => import('./routes/routing.useQueryState')),
'/routing/useQueryState/other': lazy(() => import('./routes/routing.useQueryState.other')),
'/routing/useQueryStates': lazy(() => import('./routes/routing.useQueryStates')),
'/routing/useQueryStates/other': lazy(() => import('./routes/routing.useQueryStates.other')),
'/shallow/useQueryState': lazy(() => import('./routes/shallow.useQueryState')),
'/shallow/useQueryStates': lazy(() => import('./routes/shallow.useQueryStates')),
'/form/useQueryState': lazy(() => import('./routes/form.useQueryState')),
'/form/useQueryStates': lazy(() => import('./routes/form.useQueryStates')),
'/referential-stability/useQueryState': lazy(() => import('./routes/referential-stability.useQueryState')),
'/referential-stability/useQueryStates': lazy(() => import('./routes/referential-stability.useQueryStates')),
}

export function Router() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ReferentialStabilityUseQueryState } from 'e2e-shared/specs/referential-stability'

export default ReferentialStabilityUseQueryState
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ReferentialStabilityUseQueryStates } from 'e2e-shared/specs/referential-stability'

export default ReferentialStabilityUseQueryStates
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ReferentialStabilityUseQueryState } from 'e2e-shared/specs/referential-stability'

export default ReferentialStabilityUseQueryState
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ReferentialStabilityUseQueryStates } from 'e2e-shared/specs/referential-stability'

export default ReferentialStabilityUseQueryStates
11 changes: 11 additions & 0 deletions packages/e2e/remix/cypress/e2e/shared/referential-stability.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { testReferentialStability } from 'e2e-shared/specs/referential-stability.cy'

testReferentialStability({
path: '/referential-stability/useQueryState',
hook: 'useQueryState'
})

testReferentialStability({
path: '/referential-stability/useQueryStates',
hook: 'useQueryStates'
})
14 changes: 14 additions & 0 deletions packages/e2e/shared/specs/referential-stability.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createTest } from '../create-test'

export const testReferentialStability = createTest(
'Referential stability',
({ path }) => {
it('keeps referential stability of the setter function across updates', () => {
cy.visit(path)
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
cy.get('#state').should('have.text', 'pass')
cy.get('button').click()
cy.get('#state').should('have.text', 'pass')
})
}
)
30 changes: 30 additions & 0 deletions packages/e2e/shared/specs/referential-stability.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client'

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

export function ReferentialStabilityUseQueryState() {
const [, setState] = useQueryState('test')
const setterRef = useRef(setState)
const hasChanged = setterRef.current !== setState
return (
<>
<button onClick={() => setState('test')}>Test</button>
<div id="state">{hasChanged ? 'fail' : 'pass'}</div>
</>
)
}

export function ReferentialStabilityUseQueryStates() {
const [, setState] = useQueryStates({
test: parseAsString
})
const setterRef = useRef(setState)
const hasChanged = setterRef.current !== setState
return (
<>
<button onClick={() => setState({ test: 'test' })}>Test</button>
<div id="state">{hasChanged ? 'fail' : 'pass'}</div>
</>
)
}
12 changes: 11 additions & 1 deletion packages/nuqs/src/useQueryState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,17 @@ export function useQueryState<T = string>(
emitter.emit(key, { state: newValue, query })
return scheduleFlushToURL(adapter)
},
[key, history, shallow, scroll, throttleMs, startTransition, adapter]
[
key,
history,
shallow,
scroll,
throttleMs,
startTransition,
adapter.updateUrl,
adapter.getSearchParamsSnapshot,
adapter.rateLimitFactor
]
)
return [internalState ?? defaultValue ?? null, update]
}
Expand Down
6 changes: 4 additions & 2 deletions packages/nuqs/src/useQueryStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,14 +232,16 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
return scheduleFlushToURL(adapter)
},
[
keyMap,
stateKeys,
history,
shallow,
scroll,
throttleMs,
startTransition,
resolvedUrlKeys,
adapter,
adapter.updateUrl,
adapter.getSearchParamsSnapshot,
adapter.rateLimitFactor,
defaultValues
]
)
Expand Down

0 comments on commit c089be2

Please sign in to comment.