Skip to content

Commit

Permalink
fix: Consider queued updates for initial state
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
franky47 committed Oct 4, 2023
1 parent 9402182 commit e10c85c
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 4 deletions.
101 changes: 101 additions & 0 deletions src/app/demos/repro-359/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// https://github.com/47ng/next-usequerystate/issues/359

'use client'

import {
parseAsString,
parseAsStringEnum,
useQueryState,
useQueryStates
} from '../../../../dist'

const Component1 = () => {
const [param] = useQueryState('param', parseAsString)
console.dir({ _: 'Component1.render', param })
return param ? param : 'null'
}

const Component2 = () => {
const [param] = useQueryState('param', parseAsString)
console.dir({ _: 'Component2.render', param })
return param ? param : 'null'
}

enum TargetComponent {
Comp1 = 'comp1',
Comp2 = 'comp2'
}

export default function Home() {
const [_param, setParam] = useQueryState('param', parseAsString)
const [component, seComponent] = useQueryState(
'component',
parseAsStringEnum(Object.values(TargetComponent))
)
const [multiple, setMultiple] = useQueryStates({
param: parseAsString,
component: parseAsStringEnum(Object.values(TargetComponent))
})
console.dir({ _: 'Home.render', _param, component, multiple })
return (
<>
<h1>
Repro for issue{' '}
<a href="https://github.com/47ng/next-usequerystate/issues/359">#359</a>
</h1>
<div className="p-5 border">
{component === TargetComponent.Comp1 ? <Component1 /> : null}
{component === TargetComponent.Comp2 ? <Component2 /> : null}
</div>
<div className="flex gap-2">
<button
onClick={() => {
setParam('Component1')
seComponent(TargetComponent.Comp1)
}}
className="border p-2"
>
Component 1 (nuqs)
</button>
<button
onClick={() => {
console.log('aaa')
setParam('Component2')
seComponent(TargetComponent.Comp2)
}}
className="border p-2"
>
Component 2 (nuqs)
</button>
<br />
<button
onClick={() => {
setMultiple({
param: 'Component1',
component: TargetComponent.Comp1
})
}}
className="border p-2"
>
Component 1 (nuq+)
</button>
<button
onClick={() => {
setMultiple({
param: 'Component2',
component: TargetComponent.Comp2
})
}}
className="border p-2"
>
Component 2 (nuq+)
</button>
</div>
<p>
<a href="https://github.com/47ng/next-usequerystate/blob/next/src/app/demos/repro-359/page.tsx">
Source on GitHub
</a>
</p>
</>
)
}
1 change: 1 addition & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const demos = [
'app/hex-colors',
'app/compound-parsers',
'app/crosslink',
'app/repro-359',
// Pages router demos
'pages/server-side-counter'
]
Expand Down
4 changes: 4 additions & 0 deletions src/lib/update-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export function enqueueQueryStringUpdate<Value>(
}
}

export function getInitialStateFromQueue(key: string) {
return updateQueue.get(key) ?? null
}

/**
* Eventually flush the update queue to the URL query string.
*
Expand Down
10 changes: 8 additions & 2 deletions src/lib/useQueryState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import React from 'react'
import type { Options } from './defs'
import type { Parser } from './parsers'
import { SYNC_EVENT_KEY, emitter } from './sync'
import { enqueueQueryStringUpdate, flushToURL } from './update-queue'
import {
enqueueQueryStringUpdate,
flushToURL,
getInitialStateFromQueue
} from './update-queue'

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

Expand Down Expand Up @@ -216,12 +220,14 @@ export function useQueryState<T = string>(
}`
)
const [internalState, setInternalState] = React.useState<T | null>(() => {
const value =
const queueValue = getInitialStateFromQueue(key)
const urlValue =
typeof window !== 'object'
? // SSR
initialSearchParams?.get(key) ?? null
: // Components mounted after page load must use the current URL value
new URLSearchParams(window.location.search).get(key) ?? null
const value = queueValue ?? urlValue
return value === null ? null : parse(value)
})
const stateRef = React.useRef(internalState)
Expand Down
10 changes: 8 additions & 2 deletions src/lib/useQueryStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import React from 'react'
import type { Nullable, Options } from './defs'
import type { Parser } from './parsers'
import { SYNC_EVENT_KEY, emitter } from './sync'
import { enqueueQueryStringUpdate, flushToURL } from './update-queue'
import {
enqueueQueryStringUpdate,
flushToURL,
getInitialStateFromQueue
} from './update-queue'

type KeyMapValue<Type> = Parser<Type> & {
defaultValue?: Type
Expand Down Expand Up @@ -192,7 +196,9 @@ function parseMap<KeyMap extends UseQueryStatesKeysMap>(
) {
return Object.keys(keyMap).reduce((obj, key) => {
const { defaultValue, parse } = keyMap[key]
const query = searchParams?.get(key) ?? null
const urlQuery = searchParams?.get(key) ?? null
const queueQuery = getInitialStateFromQueue(key)
const query = queueQuery ?? urlQuery
const value = query === null ? null : parse(query)
obj[key as keyof KeyMap] = value ?? defaultValue ?? null
return obj
Expand Down

0 comments on commit e10c85c

Please sign in to comment.