Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: React Router stale values after remount #887

Merged
merged 3 commits into from
Jan 31, 2025
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
9 changes: 9 additions & 0 deletions packages/e2e/shared/specs/conditional-rendering.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,14 @@ export const testConditionalRendering = createTest(
cy.get('button#mount').click()
cy.get('#state').should('have.text', 'pass')
})
it('should keep the correct state after unmounting and remounting with a different state', () => {
cy.visit(path + '?test=init')
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
cy.get('button#mount').click()
cy.get('button#set').click()
cy.get('button#unmount').click()
cy.get('button#mount').click()
cy.get('#state').should('have.text', 'pass')
})
}
)
44 changes: 31 additions & 13 deletions packages/nuqs/src/adapters/lib/react-router.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import mitt from 'mitt'
import { startTransition, useCallback, useEffect, useState } from 'react'
import { renderQueryString } from '../../url-encoding'
import { createAdapterProvider } from './context'
import type { AdapterInterface, AdapterOptions } from './defs'
import {
patchHistory as applyHistoryPatch,
historyUpdateMarker,
patchHistory,
type SearchParamsSyncEmitter
} from './patch-history'

Expand All @@ -24,11 +25,17 @@ type UseSearchParams = (initial: URLSearchParams) => [URLSearchParams, {}]

// --

export function createReactRouterBasedAdapter(
adapter: string,
useNavigate: UseNavigate,
type CreateReactRouterBasedAdapterArgs = {
adapter: string
useNavigate: UseNavigate
useSearchParams: UseSearchParams
) {
}

export function createReactRouterBasedAdapter({
adapter,
useNavigate,
useSearchParams
}: CreateReactRouterBasedAdapterArgs) {
const emitter: SearchParamsSyncEmitter = mitt()
function useNuqsReactRouterBasedAdapter(): AdapterInterface {
const navigate = useNavigate()
Expand Down Expand Up @@ -78,11 +85,23 @@ export function createReactRouterBasedAdapter(
}
function useOptimisticSearchParams() {
const [serverSearchParams] = useSearchParams(
// Note: this will only be taken into account the first time the hook is called,
// and cached for subsequent calls, causing problems when mounting components
// after shallow updates have occurred.
typeof location === 'undefined'
? new URLSearchParams()
: new URLSearchParams(location.search)
)
const [searchParams, setSearchParams] = useState(serverSearchParams)
const [searchParams, setSearchParams] = useState(() => {
if (typeof location === 'undefined') {
// We use this on the server to SSR with the correct search params.
return serverSearchParams
}
// Since useSearchParams isn't reactive to shallow changes,
// it doesn't pick up changes in the URL on mount, so we need to initialise
// the reactive state with the current URL instead.
return new URLSearchParams(location.search)
})
useEffect(() => {
function onPopState() {
setSearchParams(new URLSearchParams(location.search))
Expand All @@ -100,19 +119,18 @@ export function createReactRouterBasedAdapter(
return searchParams
}
/**
* Opt-in to syncing shallow updates of the URL with the useOptimisticSearchParams hook.
* Sync shallow updates of the URL with the useOptimisticSearchParams hook.
*
* By default, the useOptimisticSearchParams hook will only react to internal nuqs updates.
* If third party code updates the History API directly, use this function to
* enable useOptimisticSearchParams to react to those changes.
*
* Note: this is actually required in React Router frameworks to follow Link navigations.
*/
function enableHistorySync() {
patchHistory(emitter, adapter)
}
applyHistoryPatch(emitter, adapter)

return {
useNuqsReactRouterBasedAdapter,
useOptimisticSearchParams,
enableHistorySync
NuqsAdapter: createAdapterProvider(useNuqsReactRouterBasedAdapter),
useOptimisticSearchParams
}
}
22 changes: 6 additions & 16 deletions packages/nuqs/src/adapters/react-router/v6.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import { useNavigate, useSearchParams } from 'react-router-dom'
import { createAdapterProvider } from '../lib/context'
import { createReactRouterBasedAdapter } from '../lib/react-router'

const {
enableHistorySync,
useNuqsReactRouterBasedAdapter: useNuqsReactRouterV6Adapter,
useOptimisticSearchParams
} = createReactRouterBasedAdapter(
'react-router-v6',
useNavigate,
useSearchParams
)

export { useOptimisticSearchParams }

export const NuqsAdapter = createAdapterProvider(useNuqsReactRouterV6Adapter)

enableHistorySync()
export const { NuqsAdapter, useOptimisticSearchParams } =
createReactRouterBasedAdapter({
adapter: 'react-router-v6',
useNavigate,
useSearchParams
})
22 changes: 6 additions & 16 deletions packages/nuqs/src/adapters/react-router/v7.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import { useNavigate, useSearchParams } from 'react-router'
import { createAdapterProvider } from '../lib/context'
import { createReactRouterBasedAdapter } from '../lib/react-router'

const {
enableHistorySync,
useNuqsReactRouterBasedAdapter: useNuqsReactRouterV7Adapter,
useOptimisticSearchParams
} = createReactRouterBasedAdapter(
'react-router-v7',
useNavigate,
useSearchParams
)

export { useOptimisticSearchParams }

export const NuqsAdapter = createAdapterProvider(useNuqsReactRouterV7Adapter)

enableHistorySync()
export const { NuqsAdapter, useOptimisticSearchParams } =
createReactRouterBasedAdapter({
adapter: 'react-router-v7',
useNavigate,
useSearchParams
})
18 changes: 6 additions & 12 deletions packages/nuqs/src/adapters/remix.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { useNavigate, useSearchParams } from '@remix-run/react'
import { createAdapterProvider } from './lib/context'
import { createReactRouterBasedAdapter } from './lib/react-router'

const {
enableHistorySync,
useNuqsReactRouterBasedAdapter: useNuqsRemixAdapter,
useOptimisticSearchParams
} = createReactRouterBasedAdapter('remix', useNavigate, useSearchParams)

export { useOptimisticSearchParams }

export const NuqsAdapter = createAdapterProvider(useNuqsRemixAdapter)

enableHistorySync()
export const { NuqsAdapter, useOptimisticSearchParams } =
createReactRouterBasedAdapter({
adapter: 'remix',
useNavigate,
useSearchParams
})
Loading