Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion packages/nuqs/src/useQueryStates.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { act, render, renderHook, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React, { createElement, useState, type ReactNode } from 'react'
import React, {
createElement,
useState,
type ReactNode,
useEffect
} from 'react'
import { describe, expect, it, vi } from 'vitest'
import {
NullDetector,
Expand Down Expand Up @@ -201,6 +206,48 @@ describe('useQueryStates: referential equality', () => {
})
})

describe('useQueryStates: rendering & bail-out', () => {
it('should bail out of rendering the same component when setting to the same value', async () => {
let renderCount = 0
function TestComponent() {
const [{ test }, setSearchParams] = useQueryStates({
test: parseAsString
})
useEffect(() => {
renderCount++
})
return (
<>
<button
onClick={() => {
setSearchParams(v => v)
}}
>
Start
</button>
<div>value: {test}</div>
</>
)
}
const user = userEvent.setup()
const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
render(<TestComponent />, {
wrapper: withNuqsTestingAdapter({
onUrlUpdate,
searchParams: '?test=init'
})
})
await expect(screen.findByText('value: init')).resolves.toBeInTheDocument()
expect(renderCount).toBe(1)
expect(onUrlUpdate).toHaveBeenCalledTimes(0)

await user.click(screen.getByRole('button', { name: 'Start' }))

expect(renderCount).toBe(1) // same render count as before
expect(onUrlUpdate).toHaveBeenCalledTimes(1) // url update is still called
})
})

describe('useQueryStates: urlKeys remapping', () => {
it('uses the object key names by default', async () => {
const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
Expand Down
61 changes: 37 additions & 24 deletions packages/nuqs/src/useQueryStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,36 +194,49 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(

// Sync all hooks together & with external URL changes
useEffect(() => {
function updateInternalState(state: V) {
debug('[nuq+ %s `%s`] updateInternalState %O', hookId, stateKeys, state)
stateRef.current = state
setInternalState(state)
}
const handlers = Object.keys(keyMap).reduce(
(handlers, stateKey) => {
handlers[stateKey as keyof KeyMap] = ({
state,
query
}: CrossHookSyncPayload) => {
const { defaultValue } = keyMap[stateKey]!
const urlKey = resolvedUrlKeys[stateKey]!
// Note: cannot mutate in-place, the object ref must change
// for the subsequent setState to pick it up.
stateRef.current = {
...stateRef.current,
[stateKey as keyof KeyMap]: state ?? defaultValue ?? null
}
queryRef.current[urlKey] = query
debug(
'[nuq+ %s `%s`] Cross-hook key sync %s: %O (default: %O). Resolved: %O',
hookId,
stateKeys,
urlKey,
state,
defaultValue,
stateRef.current
)
updateInternalState(stateRef.current)
setInternalState(currentState => {
const { defaultValue } = keyMap[stateKey]!
const urlKey = resolvedUrlKeys[stateKey]!
const nextValue = state ?? defaultValue ?? null
const currentValue = currentState[stateKey] ?? defaultValue ?? null

if (Object.is(currentValue, nextValue)) {
debug(
'[nuq+ %s `%s`] Cross-hook key sync %s: %O (default: %O). no change, skipping, resolved: %O',
hookId,
stateKeys,
urlKey,
state,
defaultValue,
stateRef.current
)
// bail out by returning the current state
return currentState
}
// Note: cannot mutate in-place, the object ref must change
// for the subsequent setState to pick it up.
stateRef.current = {
...stateRef.current,
[stateKey as keyof KeyMap]: nextValue
}
queryRef.current[urlKey] = query
debug(
'[nuq+ %s `%s`] Cross-hook key sync %s: %O (default: %O). updateInternalState, resolved: %O',
hookId,
stateKeys,
urlKey,
state,
defaultValue,
stateRef.current
)
return stateRef.current
})
}
return handlers
},
Expand Down
Loading