diff --git a/packages/docs/content/docs/adapters.mdx b/packages/docs/content/docs/adapters.mdx index 6ec4adf0..0dd0b04a 100644 --- a/packages/docs/content/docs/adapters.mdx +++ b/packages/docs/content/docs/adapters.mdx @@ -127,6 +127,14 @@ export function ReactRouter() { } ``` + + + Only `BrowserRouter` is supported. There may be support for `HashRouter` + in the future (see issue [#810](https://github.com/47ng/nuqs/issues/810)), but + support for `MemoryRouter` is not planned. + + + ## React Router v7 ```tsx title="app/root.tsx" @@ -151,6 +159,9 @@ export default function App() { Please pin your imports to the specific version, eg: `nuqs/adapters/react-router/v6` or `nuqs/adapters/react-router/v7`. + + The main difference is where the React Router hooks are imported from: + `react-router-dom` for v6, and `react-router` for v7. ## Testing diff --git a/packages/e2e/react-router/v6/cypress/e2e/repro-839.cy.ts b/packages/e2e/react-router/v6/cypress/e2e/repro-839.cy.ts new file mode 100644 index 00000000..ea5a1912 --- /dev/null +++ b/packages/e2e/react-router/v6/cypress/e2e/repro-839.cy.ts @@ -0,0 +1,5 @@ +import { testRepro839LocationStatePersistence } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence.cy' + +testRepro839LocationStatePersistence({ + path: '/repro-839' +}) diff --git a/packages/e2e/react-router/v6/src/react-router.tsx b/packages/e2e/react-router/v6/src/react-router.tsx index 8dfb65b6..95a2647a 100644 --- a/packages/e2e/react-router/v6/src/react-router.tsx +++ b/packages/e2e/react-router/v6/src/react-router.tsx @@ -20,6 +20,7 @@ function load(mod: Promise<{ default: any; [otherExports: string]: any }>) { const router = createBrowserRouter( createRoutesFromElements( } >, + {/* Shared E2E tests */} @@ -40,6 +41,8 @@ const router = createBrowserRouter( + {/* Reproductions */} + )) diff --git a/packages/e2e/react-router/v6/src/routes/repro-839.tsx b/packages/e2e/react-router/v6/src/routes/repro-839.tsx new file mode 100644 index 00000000..8b608aa0 --- /dev/null +++ b/packages/e2e/react-router/v6/src/routes/repro-839.tsx @@ -0,0 +1,6 @@ +import { Repro839 } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence' +import { useLocation, useNavigate } from 'react-router-dom' + +export default function Page() { + return +} diff --git a/packages/e2e/react-router/v7/app/routes.ts b/packages/e2e/react-router/v7/app/routes.ts index 6cebf5a3..03299a3a 100644 --- a/packages/e2e/react-router/v7/app/routes.ts +++ b/packages/e2e/react-router/v7/app/routes.ts @@ -3,6 +3,7 @@ import { type RouteConfig, layout, route } from '@react-router/dev/routes' export default [ // prettier-ignore layout('layout.tsx', [ + // Shared E2E tests route('/hash-preservation', './routes/hash-preservation.tsx'), route('/basic-io/useQueryState', './routes/basic-io.useQueryState.tsx'), route('/basic-io/useQueryStates', './routes/basic-io.useQueryStates.tsx'), @@ -22,6 +23,8 @@ export default [ route('/form/useQueryState', './routes/form.useQueryState.tsx'), route('/form/useQueryStates', './routes/form.useQueryStates.tsx'), route('/referential-stability/useQueryState', './routes/referential-stability.useQueryState.tsx'), - route('/referential-stability/useQueryStates', './routes/referential-stability.useQueryStates.tsx') + route('/referential-stability/useQueryStates', './routes/referential-stability.useQueryStates.tsx'), + // Reproductions + route('/repro-839', './routes/repro-839.tsx'), ]) ] satisfies RouteConfig diff --git a/packages/e2e/react-router/v7/app/routes/repro-839.tsx b/packages/e2e/react-router/v7/app/routes/repro-839.tsx new file mode 100644 index 00000000..33f6314e --- /dev/null +++ b/packages/e2e/react-router/v7/app/routes/repro-839.tsx @@ -0,0 +1,6 @@ +import { Repro839 } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence' +import { useLocation, useNavigate } from 'react-router' + +export default function Page() { + return +} diff --git a/packages/e2e/react-router/v7/cypress/e2e/repro-839.cy.ts b/packages/e2e/react-router/v7/cypress/e2e/repro-839.cy.ts new file mode 100644 index 00000000..ea5a1912 --- /dev/null +++ b/packages/e2e/react-router/v7/cypress/e2e/repro-839.cy.ts @@ -0,0 +1,5 @@ +import { testRepro839LocationStatePersistence } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence.cy' + +testRepro839LocationStatePersistence({ + path: '/repro-839' +}) diff --git a/packages/e2e/remix/app/routes/repro-839.tsx b/packages/e2e/remix/app/routes/repro-839.tsx new file mode 100644 index 00000000..541dc5da --- /dev/null +++ b/packages/e2e/remix/app/routes/repro-839.tsx @@ -0,0 +1,6 @@ +import { useLocation, useNavigate } from '@remix-run/react' +import { Repro839 } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence' + +export default function Page() { + return +} diff --git a/packages/e2e/remix/cypress/e2e/repro-839.cy.ts b/packages/e2e/remix/cypress/e2e/repro-839.cy.ts new file mode 100644 index 00000000..ea5a1912 --- /dev/null +++ b/packages/e2e/remix/cypress/e2e/repro-839.cy.ts @@ -0,0 +1,5 @@ +import { testRepro839LocationStatePersistence } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence.cy' + +testRepro839LocationStatePersistence({ + path: '/repro-839' +}) diff --git a/packages/e2e/shared/specs/react-router/repro-839-location-state-persistence.cy.ts b/packages/e2e/shared/specs/react-router/repro-839-location-state-persistence.cy.ts new file mode 100644 index 00000000..5b3a36d3 --- /dev/null +++ b/packages/e2e/shared/specs/react-router/repro-839-location-state-persistence.cy.ts @@ -0,0 +1,22 @@ +import { createTest } from '../../create-test' + +export const testRepro839LocationStatePersistence = createTest( + 'Repro for issue #839 - Location state persistence', + ({ path }) => { + it('persists location.state on shallow URL updates', () => { + cy.visit(path) + cy.contains('#hydration-marker', 'hydrated').should('be.hidden') + cy.get('#setup').click() + cy.get('#shallow').click() + cy.get('#state').should('have.text', '{"test":"pass"}') + }) + + it('persists location.state on deep URL updates', () => { + cy.visit(path) + cy.contains('#hydration-marker', 'hydrated').should('be.hidden') + cy.get('#setup').click() + cy.get('#deep').click() + cy.get('#state').should('have.text', '{"test":"pass"}') + }) + } +) diff --git a/packages/e2e/shared/specs/react-router/repro-839-location-state-persistence.tsx b/packages/e2e/shared/specs/react-router/repro-839-location-state-persistence.tsx new file mode 100644 index 00000000..0ecd6026 --- /dev/null +++ b/packages/e2e/shared/specs/react-router/repro-839-location-state-persistence.tsx @@ -0,0 +1,34 @@ +import { useQueryState } from 'nuqs' + +type Repro839Props = { + useNavigate: () => (url: string, options: { state: unknown }) => void + useLocation: () => { state: unknown } +} + +export function Repro839({ useNavigate, useLocation }: Repro839Props) { + const navigate = useNavigate() + const location = useLocation() + const [, setShallow] = useQueryState('shallow', { + shallow: true + }) + const [, setDeep] = useQueryState('deep', { + shallow: false + }) + return ( + <> + + + +
{JSON.stringify(location.state)}
+ + ) +} diff --git a/packages/nuqs/src/adapters/lib/react-router.ts b/packages/nuqs/src/adapters/lib/react-router.ts index 846d6758..408b2df6 100644 --- a/packages/nuqs/src/adapters/lib/react-router.ts +++ b/packages/nuqs/src/adapters/lib/react-router.ts @@ -16,6 +16,7 @@ type NavigateUrl = { type NavigateOptions = { replace?: boolean preventScrollReset?: boolean + state?: unknown } type NavigateFn = (url: NavigateUrl, options: NavigateOptions) => void type UseNavigate = () => NavigateFn @@ -60,7 +61,8 @@ export function createReactRouterBasedAdapter( }, { replace: true, - preventScrollReset: true + preventScrollReset: true, + state: history.state?.usr } ) }