Skip to content

Commit c089be2

Browse files
authored
fix: Referential stability for the state updater function (#841)
1 parent 10e526d commit c089be2

24 files changed

+237
-56
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { testReferentialStability } from 'e2e-shared/specs/referential-stability.cy'
2+
3+
testReferentialStability({
4+
path: '/app/referential-stability/useQueryState',
5+
hook: 'useQueryState',
6+
nextJsRouter: 'app'
7+
})
8+
9+
testReferentialStability({
10+
path: '/app/referential-stability/useQueryStates',
11+
hook: 'useQueryStates',
12+
nextJsRouter: 'app'
13+
})
14+
15+
testReferentialStability({
16+
path: '/pages/referential-stability/useQueryState',
17+
hook: 'useQueryState',
18+
nextJsRouter: 'pages'
19+
})
20+
21+
testReferentialStability({
22+
path: '/pages/referential-stability/useQueryStates',
23+
hook: 'useQueryStates',
24+
nextJsRouter: 'pages'
25+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ReferentialStabilityUseQueryState } from 'e2e-shared/specs/referential-stability'
2+
import { Suspense } from 'react'
3+
4+
export default function Page() {
5+
return (
6+
<Suspense>
7+
<ReferentialStabilityUseQueryState />
8+
</Suspense>
9+
)
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ReferentialStabilityUseQueryStates } from 'e2e-shared/specs/referential-stability'
2+
import { Suspense } from 'react'
3+
4+
export default function Page() {
5+
return (
6+
<Suspense>
7+
<ReferentialStabilityUseQueryStates />
8+
</Suspense>
9+
)
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ReferentialStabilityUseQueryState } from 'e2e-shared/specs/referential-stability'
2+
3+
export default ReferentialStabilityUseQueryState
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ReferentialStabilityUseQueryStates } from 'e2e-shared/specs/referential-stability'
2+
3+
export default ReferentialStabilityUseQueryStates
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { testReferentialStability } from 'e2e-shared/specs/referential-stability.cy'
2+
3+
testReferentialStability({
4+
path: '/referential-stability/useQueryState',
5+
hook: 'useQueryState'
6+
})
7+
8+
testReferentialStability({
9+
path: '/referential-stability/useQueryStates',
10+
hook: 'useQueryStates'
11+
})

packages/e2e/react-router/v6/src/react-router.tsx

+20-18
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,26 @@ function load(mod: Promise<{ default: any; [otherExports: string]: any }>) {
2020
const router = createBrowserRouter(
2121
createRoutesFromElements(
2222
<Route path="/" element={<RootLayout/>} >,
23-
<Route path='hash-preservation' lazy={load(import('./routes/hash-preservation'))} />
24-
<Route path='basic-io/useQueryState' lazy={load(import('./routes/basic-io.useQueryState'))} />
25-
<Route path='basic-io/useQueryStates' lazy={load(import('./routes/basic-io.useQueryStates'))} />
26-
<Route path='linking/useQueryState' lazy={load(import('./routes/linking.useQueryState'))} />
27-
<Route path='linking/useQueryState/other' lazy={load(import('./routes/linking.useQueryState.other'))} />
28-
<Route path='linking/useQueryStates' lazy={load(import('./routes/linking.useQueryStates'))} />
29-
<Route path='linking/useQueryStates/other' lazy={load(import('./routes/linking.useQueryStates.other'))} />
30-
<Route path='push/useQueryState' lazy={load(import('./routes/push.useQueryState'))} />
31-
<Route path='push/useQueryStates' lazy={load(import('./routes/push.useQueryStates'))} />
32-
<Route path="routing/useQueryState" lazy={load(import('./routes/routing.useQueryState'))} />
33-
<Route path="routing/useQueryState/other" lazy={load(import('./routes/routing.useQueryState.other'))} />
34-
<Route path="routing/useQueryStates" lazy={load(import('./routes/routing.useQueryStates'))} />
35-
<Route path="routing/useQueryStates/other" lazy={load(import('./routes/routing.useQueryStates.other'))} />
36-
<Route path='shallow/useQueryState' lazy={load(import('./routes/shallow.useQueryState'))} />
37-
<Route path='shallow/useQueryStates' lazy={load(import('./routes/shallow.useQueryStates'))} />
38-
<Route path='loader' lazy={load(import('./routes/loader'))} />
39-
<Route path="form/useQueryState" lazy={load(import('./routes/form.useQueryState'))} />
40-
<Route path="form/useQueryStates" lazy={load(import('./routes/form.useQueryStates'))} />
23+
<Route path='hash-preservation' lazy={load(import('./routes/hash-preservation'))} />
24+
<Route path='basic-io/useQueryState' lazy={load(import('./routes/basic-io.useQueryState'))} />
25+
<Route path='basic-io/useQueryStates' lazy={load(import('./routes/basic-io.useQueryStates'))} />
26+
<Route path='linking/useQueryState' lazy={load(import('./routes/linking.useQueryState'))} />
27+
<Route path='linking/useQueryState/other' lazy={load(import('./routes/linking.useQueryState.other'))} />
28+
<Route path='linking/useQueryStates' lazy={load(import('./routes/linking.useQueryStates'))} />
29+
<Route path='linking/useQueryStates/other' lazy={load(import('./routes/linking.useQueryStates.other'))} />
30+
<Route path='push/useQueryState' lazy={load(import('./routes/push.useQueryState'))} />
31+
<Route path='push/useQueryStates' lazy={load(import('./routes/push.useQueryStates'))} />
32+
<Route path="routing/useQueryState" lazy={load(import('./routes/routing.useQueryState'))} />
33+
<Route path="routing/useQueryState/other" lazy={load(import('./routes/routing.useQueryState.other'))} />
34+
<Route path="routing/useQueryStates" lazy={load(import('./routes/routing.useQueryStates'))} />
35+
<Route path="routing/useQueryStates/other" lazy={load(import('./routes/routing.useQueryStates.other'))} />
36+
<Route path='shallow/useQueryState' lazy={load(import('./routes/shallow.useQueryState'))} />
37+
<Route path='shallow/useQueryStates' lazy={load(import('./routes/shallow.useQueryStates'))} />
38+
<Route path='loader' lazy={load(import('./routes/loader'))} />
39+
<Route path="form/useQueryState" lazy={load(import('./routes/form.useQueryState'))} />
40+
<Route path="form/useQueryStates" lazy={load(import('./routes/form.useQueryStates'))} />
41+
<Route path="referential-stability/useQueryState" lazy={load(import('./routes/referential-stability.useQueryState'))} />
42+
<Route path="referential-stability/useQueryStates" lazy={load(import('./routes/referential-stability.useQueryStates'))} />
4143
</Route>
4244
))
4345

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

packages/e2e/react-router/v7/app/routes.ts

+20-18
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,25 @@ import { type RouteConfig, layout, route } from '@react-router/dev/routes'
33
export default [
44
// prettier-ignore
55
layout('layout.tsx', [
6-
route('/hash-preservation', './routes/hash-preservation.tsx'),
7-
route('/basic-io/useQueryState', './routes/basic-io.useQueryState.tsx'),
8-
route('/basic-io/useQueryStates', './routes/basic-io.useQueryStates.tsx'),
9-
route('/linking/useQueryState', './routes/linking.useQueryState.tsx'),
10-
route('/linking/useQueryState/other', './routes/linking.useQueryState.other.tsx'),
11-
route('/linking/useQueryStates', './routes/linking.useQueryStates.tsx'),
12-
route('/linking/useQueryStates/other', './routes/linking.useQueryStates.other.tsx'),
13-
route('/push/useQueryState', './routes/push.useQueryState.tsx'),
14-
route('/push/useQueryStates', './routes/push.useQueryStates.tsx'),
15-
route('/routing/useQueryState', './routes/routing.useQueryState.tsx'),
16-
route('/routing/useQueryState/other', './routes/routing.useQueryState.other.tsx'),
17-
route('/routing/useQueryStates', './routes/routing.useQueryStates.tsx'),
18-
route('/routing/useQueryStates/other', './routes/routing.useQueryStates.other.tsx'),
19-
route('/shallow/useQueryState', './routes/shallow.useQueryState.tsx'),
20-
route('/shallow/useQueryStates', './routes/shallow.useQueryStates.tsx'),
21-
route('/loader', './routes/loader.tsx'),
22-
route('/form/useQueryState', './routes/form.useQueryState.tsx'),
23-
route('/form/useQueryStates', './routes/form.useQueryStates.tsx'),
6+
route('/hash-preservation', './routes/hash-preservation.tsx'),
7+
route('/basic-io/useQueryState', './routes/basic-io.useQueryState.tsx'),
8+
route('/basic-io/useQueryStates', './routes/basic-io.useQueryStates.tsx'),
9+
route('/linking/useQueryState', './routes/linking.useQueryState.tsx'),
10+
route('/linking/useQueryState/other', './routes/linking.useQueryState.other.tsx'),
11+
route('/linking/useQueryStates', './routes/linking.useQueryStates.tsx'),
12+
route('/linking/useQueryStates/other', './routes/linking.useQueryStates.other.tsx'),
13+
route('/push/useQueryState', './routes/push.useQueryState.tsx'),
14+
route('/push/useQueryStates', './routes/push.useQueryStates.tsx'),
15+
route('/routing/useQueryState', './routes/routing.useQueryState.tsx'),
16+
route('/routing/useQueryState/other', './routes/routing.useQueryState.other.tsx'),
17+
route('/routing/useQueryStates', './routes/routing.useQueryStates.tsx'),
18+
route('/routing/useQueryStates/other', './routes/routing.useQueryStates.other.tsx'),
19+
route('/shallow/useQueryState', './routes/shallow.useQueryState.tsx'),
20+
route('/shallow/useQueryStates', './routes/shallow.useQueryStates.tsx'),
21+
route('/loader', './routes/loader.tsx'),
22+
route('/form/useQueryState', './routes/form.useQueryState.tsx'),
23+
route('/form/useQueryStates', './routes/form.useQueryStates.tsx'),
24+
route('/referential-stability/useQueryState', './routes/referential-stability.useQueryState.tsx'),
25+
route('/referential-stability/useQueryStates', './routes/referential-stability.useQueryStates.tsx')
2426
])
2527
] satisfies RouteConfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ReferentialStabilityUseQueryState } from 'e2e-shared/specs/referential-stability'
2+
3+
export default ReferentialStabilityUseQueryState
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ReferentialStabilityUseQueryStates } from 'e2e-shared/specs/referential-stability'
2+
3+
export default ReferentialStabilityUseQueryStates
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { testReferentialStability } from 'e2e-shared/specs/referential-stability.cy'
2+
3+
testReferentialStability({
4+
path: '/referential-stability/useQueryState',
5+
hook: 'useQueryState'
6+
})
7+
8+
testReferentialStability({
9+
path: '/referential-stability/useQueryStates',
10+
hook: 'useQueryStates'
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { testReferentialStability } from 'e2e-shared/specs/referential-stability.cy'
2+
3+
testReferentialStability({
4+
path: '/referential-stability/useQueryState',
5+
hook: 'useQueryState'
6+
})
7+
8+
testReferentialStability({
9+
path: '/referential-stability/useQueryStates',
10+
hook: 'useQueryStates'
11+
})

packages/e2e/react/src/routes.tsx

+19-17
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,25 @@ import { JSX, lazy } from 'react'
22

33
// prettier-ignore
44
const routes: Record<string, React.LazyExoticComponent<() => JSX.Element>> = {
5-
'/hash-preservation': lazy(() => import('./routes/hash-preservation')),
6-
'/basic-io/useQueryState': lazy(() => import('./routes/basic-io.useQueryState')),
7-
'/basic-io/useQueryStates': lazy(() => import('./routes/basic-io.useQueryStates')),
8-
'/push/useQueryState': lazy(() => import('./routes/push.useQueryState')),
9-
'/push/useQueryStates': lazy(() => import('./routes/push.useQueryStates')),
10-
'/linking/useQueryState': lazy(() => import('./routes/linking.useQueryState')),
11-
'/linking/useQueryState/other': lazy(() => import('./routes/linking.useQueryState.other')),
12-
'/linking/useQueryStates': lazy(() => import('./routes/linking.useQueryStates')),
13-
'/linking/useQueryStates/other': lazy(() => import('./routes/linking.useQueryStates.other')),
14-
'/routing/useQueryState': lazy(() => import('./routes/routing.useQueryState')),
15-
'/routing/useQueryState/other': lazy(() => import('./routes/routing.useQueryState.other')),
16-
'/routing/useQueryStates': lazy(() => import('./routes/routing.useQueryStates')),
17-
'/routing/useQueryStates/other': lazy(() => import('./routes/routing.useQueryStates.other')),
18-
'/shallow/useQueryState': lazy(() => import('./routes/shallow.useQueryState')),
19-
'/shallow/useQueryStates': lazy(() => import('./routes/shallow.useQueryStates')),
20-
'/form/useQueryState': lazy(() => import('./routes/form.useQueryState')),
21-
'/form/useQueryStates': lazy(() => import('./routes/form.useQueryStates')),
5+
'/hash-preservation': lazy(() => import('./routes/hash-preservation')),
6+
'/basic-io/useQueryState': lazy(() => import('./routes/basic-io.useQueryState')),
7+
'/basic-io/useQueryStates': lazy(() => import('./routes/basic-io.useQueryStates')),
8+
'/push/useQueryState': lazy(() => import('./routes/push.useQueryState')),
9+
'/push/useQueryStates': lazy(() => import('./routes/push.useQueryStates')),
10+
'/linking/useQueryState': lazy(() => import('./routes/linking.useQueryState')),
11+
'/linking/useQueryState/other': lazy(() => import('./routes/linking.useQueryState.other')),
12+
'/linking/useQueryStates': lazy(() => import('./routes/linking.useQueryStates')),
13+
'/linking/useQueryStates/other': lazy(() => import('./routes/linking.useQueryStates.other')),
14+
'/routing/useQueryState': lazy(() => import('./routes/routing.useQueryState')),
15+
'/routing/useQueryState/other': lazy(() => import('./routes/routing.useQueryState.other')),
16+
'/routing/useQueryStates': lazy(() => import('./routes/routing.useQueryStates')),
17+
'/routing/useQueryStates/other': lazy(() => import('./routes/routing.useQueryStates.other')),
18+
'/shallow/useQueryState': lazy(() => import('./routes/shallow.useQueryState')),
19+
'/shallow/useQueryStates': lazy(() => import('./routes/shallow.useQueryStates')),
20+
'/form/useQueryState': lazy(() => import('./routes/form.useQueryState')),
21+
'/form/useQueryStates': lazy(() => import('./routes/form.useQueryStates')),
22+
'/referential-stability/useQueryState': lazy(() => import('./routes/referential-stability.useQueryState')),
23+
'/referential-stability/useQueryStates': lazy(() => import('./routes/referential-stability.useQueryStates')),
2224
}
2325

2426
export function Router() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ReferentialStabilityUseQueryState } from 'e2e-shared/specs/referential-stability'
2+
3+
export default ReferentialStabilityUseQueryState
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ReferentialStabilityUseQueryStates } from 'e2e-shared/specs/referential-stability'
2+
3+
export default ReferentialStabilityUseQueryStates
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ReferentialStabilityUseQueryState } from 'e2e-shared/specs/referential-stability'
2+
3+
export default ReferentialStabilityUseQueryState
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ReferentialStabilityUseQueryStates } from 'e2e-shared/specs/referential-stability'
2+
3+
export default ReferentialStabilityUseQueryStates
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { testReferentialStability } from 'e2e-shared/specs/referential-stability.cy'
2+
3+
testReferentialStability({
4+
path: '/referential-stability/useQueryState',
5+
hook: 'useQueryState'
6+
})
7+
8+
testReferentialStability({
9+
path: '/referential-stability/useQueryStates',
10+
hook: 'useQueryStates'
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { createTest } from '../create-test'
2+
3+
export const testReferentialStability = createTest(
4+
'Referential stability',
5+
({ path }) => {
6+
it('keeps referential stability of the setter function across updates', () => {
7+
cy.visit(path)
8+
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
9+
cy.get('#state').should('have.text', 'pass')
10+
cy.get('button').click()
11+
cy.get('#state').should('have.text', 'pass')
12+
})
13+
}
14+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use client'
2+
3+
import { parseAsString, useQueryState, useQueryStates } from 'nuqs'
4+
import { useRef } from 'react'
5+
6+
export function ReferentialStabilityUseQueryState() {
7+
const [, setState] = useQueryState('test')
8+
const setterRef = useRef(setState)
9+
const hasChanged = setterRef.current !== setState
10+
return (
11+
<>
12+
<button onClick={() => setState('test')}>Test</button>
13+
<div id="state">{hasChanged ? 'fail' : 'pass'}</div>
14+
</>
15+
)
16+
}
17+
18+
export function ReferentialStabilityUseQueryStates() {
19+
const [, setState] = useQueryStates({
20+
test: parseAsString
21+
})
22+
const setterRef = useRef(setState)
23+
const hasChanged = setterRef.current !== setState
24+
return (
25+
<>
26+
<button onClick={() => setState({ test: 'test' })}>Test</button>
27+
<div id="state">{hasChanged ? 'fail' : 'pass'}</div>
28+
</>
29+
)
30+
}

packages/nuqs/src/useQueryState.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,17 @@ export function useQueryState<T = string>(
294294
emitter.emit(key, { state: newValue, query })
295295
return scheduleFlushToURL(adapter)
296296
},
297-
[key, history, shallow, scroll, throttleMs, startTransition, adapter]
297+
[
298+
key,
299+
history,
300+
shallow,
301+
scroll,
302+
throttleMs,
303+
startTransition,
304+
adapter.updateUrl,
305+
adapter.getSearchParamsSnapshot,
306+
adapter.rateLimitFactor
307+
]
298308
)
299309
return [internalState ?? defaultValue ?? null, update]
300310
}

packages/nuqs/src/useQueryStates.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -232,14 +232,16 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
232232
return scheduleFlushToURL(adapter)
233233
},
234234
[
235-
keyMap,
235+
stateKeys,
236236
history,
237237
shallow,
238238
scroll,
239239
throttleMs,
240240
startTransition,
241241
resolvedUrlKeys,
242-
adapter,
242+
adapter.updateUrl,
243+
adapter.getSearchParamsSnapshot,
244+
adapter.rateLimitFactor,
243245
defaultValues
244246
]
245247
)

0 commit comments

Comments
 (0)