Skip to content

Commit e10c85c

Browse files
committed
fix: Consider queued updates for initial state
This solves issues when a component is mounted by a state change, and which itself contains a hook on the key that caused it to mount. Because the correct value is still in the queue at mount time, it won't be correctly set in the mounted component, and because internal history updates don't trigger a state sync, the newly mounted component won't re-render when the URL is updated. Closes #359.
1 parent 9402182 commit e10c85c

File tree

5 files changed

+122
-4
lines changed

5 files changed

+122
-4
lines changed

src/app/demos/repro-359/page.tsx

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// https://github.com/47ng/next-usequerystate/issues/359
2+
3+
'use client'
4+
5+
import {
6+
parseAsString,
7+
parseAsStringEnum,
8+
useQueryState,
9+
useQueryStates
10+
} from '../../../../dist'
11+
12+
const Component1 = () => {
13+
const [param] = useQueryState('param', parseAsString)
14+
console.dir({ _: 'Component1.render', param })
15+
return param ? param : 'null'
16+
}
17+
18+
const Component2 = () => {
19+
const [param] = useQueryState('param', parseAsString)
20+
console.dir({ _: 'Component2.render', param })
21+
return param ? param : 'null'
22+
}
23+
24+
enum TargetComponent {
25+
Comp1 = 'comp1',
26+
Comp2 = 'comp2'
27+
}
28+
29+
export default function Home() {
30+
const [_param, setParam] = useQueryState('param', parseAsString)
31+
const [component, seComponent] = useQueryState(
32+
'component',
33+
parseAsStringEnum(Object.values(TargetComponent))
34+
)
35+
const [multiple, setMultiple] = useQueryStates({
36+
param: parseAsString,
37+
component: parseAsStringEnum(Object.values(TargetComponent))
38+
})
39+
console.dir({ _: 'Home.render', _param, component, multiple })
40+
return (
41+
<>
42+
<h1>
43+
Repro for issue{' '}
44+
<a href="https://github.com/47ng/next-usequerystate/issues/359">#359</a>
45+
</h1>
46+
<div className="p-5 border">
47+
{component === TargetComponent.Comp1 ? <Component1 /> : null}
48+
{component === TargetComponent.Comp2 ? <Component2 /> : null}
49+
</div>
50+
<div className="flex gap-2">
51+
<button
52+
onClick={() => {
53+
setParam('Component1')
54+
seComponent(TargetComponent.Comp1)
55+
}}
56+
className="border p-2"
57+
>
58+
Component 1 (nuqs)
59+
</button>
60+
<button
61+
onClick={() => {
62+
console.log('aaa')
63+
setParam('Component2')
64+
seComponent(TargetComponent.Comp2)
65+
}}
66+
className="border p-2"
67+
>
68+
Component 2 (nuqs)
69+
</button>
70+
<br />
71+
<button
72+
onClick={() => {
73+
setMultiple({
74+
param: 'Component1',
75+
component: TargetComponent.Comp1
76+
})
77+
}}
78+
className="border p-2"
79+
>
80+
Component 1 (nuq+)
81+
</button>
82+
<button
83+
onClick={() => {
84+
setMultiple({
85+
param: 'Component2',
86+
component: TargetComponent.Comp2
87+
})
88+
}}
89+
className="border p-2"
90+
>
91+
Component 2 (nuq+)
92+
</button>
93+
</div>
94+
<p>
95+
<a href="https://github.com/47ng/next-usequerystate/blob/next/src/app/demos/repro-359/page.tsx">
96+
Source on GitHub
97+
</a>
98+
</p>
99+
</>
100+
)
101+
}

src/app/page.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const demos = [
1010
'app/hex-colors',
1111
'app/compound-parsers',
1212
'app/crosslink',
13+
'app/repro-359',
1314
// Pages router demos
1415
'pages/server-side-counter'
1516
]

src/lib/update-queue.ts

+4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export function enqueueQueryStringUpdate<Value>(
3535
}
3636
}
3737

38+
export function getInitialStateFromQueue(key: string) {
39+
return updateQueue.get(key) ?? null
40+
}
41+
3842
/**
3943
* Eventually flush the update queue to the URL query string.
4044
*

src/lib/useQueryState.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import React from 'react'
33
import type { Options } from './defs'
44
import type { Parser } from './parsers'
55
import { SYNC_EVENT_KEY, emitter } from './sync'
6-
import { enqueueQueryStringUpdate, flushToURL } from './update-queue'
6+
import {
7+
enqueueQueryStringUpdate,
8+
flushToURL,
9+
getInitialStateFromQueue
10+
} from './update-queue'
711

812
export interface UseQueryStateOptions<T> extends Parser<T>, Options {}
913

@@ -216,12 +220,14 @@ export function useQueryState<T = string>(
216220
}`
217221
)
218222
const [internalState, setInternalState] = React.useState<T | null>(() => {
219-
const value =
223+
const queueValue = getInitialStateFromQueue(key)
224+
const urlValue =
220225
typeof window !== 'object'
221226
? // SSR
222227
initialSearchParams?.get(key) ?? null
223228
: // Components mounted after page load must use the current URL value
224229
new URLSearchParams(window.location.search).get(key) ?? null
230+
const value = queueValue ?? urlValue
225231
return value === null ? null : parse(value)
226232
})
227233
const stateRef = React.useRef(internalState)

src/lib/useQueryStates.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import React from 'react'
77
import type { Nullable, Options } from './defs'
88
import type { Parser } from './parsers'
99
import { SYNC_EVENT_KEY, emitter } from './sync'
10-
import { enqueueQueryStringUpdate, flushToURL } from './update-queue'
10+
import {
11+
enqueueQueryStringUpdate,
12+
flushToURL,
13+
getInitialStateFromQueue
14+
} from './update-queue'
1115

1216
type KeyMapValue<Type> = Parser<Type> & {
1317
defaultValue?: Type
@@ -192,7 +196,9 @@ function parseMap<KeyMap extends UseQueryStatesKeysMap>(
192196
) {
193197
return Object.keys(keyMap).reduce((obj, key) => {
194198
const { defaultValue, parse } = keyMap[key]
195-
const query = searchParams?.get(key) ?? null
199+
const urlQuery = searchParams?.get(key) ?? null
200+
const queueQuery = getInitialStateFromQueue(key)
201+
const query = queueQuery ?? urlQuery
196202
const value = query === null ? null : parse(query)
197203
obj[key as keyof KeyMap] = value ?? defaultValue ?? null
198204
return obj

0 commit comments

Comments
 (0)