Skip to content

Commit

Permalink
fix: Maintain location.state in React Router frameworks (#840)
Browse files Browse the repository at this point in the history
* test: Add failing test for issue 839

* fix: Forward user-defined location.state

* doc: Add disclaimer about router support
  • Loading branch information
franky47 authored Jan 3, 2025
1 parent c089be2 commit 39d6f4a
Show file tree
Hide file tree
Showing 12 changed files with 110 additions and 2 deletions.
11 changes: 11 additions & 0 deletions packages/docs/content/docs/adapters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ export function ReactRouter() {
}
```

<Callout>

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.

</Callout>

## React Router v7

```tsx title="app/root.tsx"
Expand All @@ -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.
</Callout>

## Testing
Expand Down
5 changes: 5 additions & 0 deletions packages/e2e/react-router/v6/cypress/e2e/repro-839.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { testRepro839LocationStatePersistence } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence.cy'

testRepro839LocationStatePersistence({
path: '/repro-839'
})
3 changes: 3 additions & 0 deletions packages/e2e/react-router/v6/src/react-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function load(mod: Promise<{ default: any; [otherExports: string]: any }>) {
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<RootLayout/>} >,
{/* Shared E2E tests */}
<Route path='hash-preservation' lazy={load(import('./routes/hash-preservation'))} />
<Route path='basic-io/useQueryState' lazy={load(import('./routes/basic-io.useQueryState'))} />
<Route path='basic-io/useQueryStates' lazy={load(import('./routes/basic-io.useQueryStates'))} />
Expand All @@ -40,6 +41,8 @@ const router = createBrowserRouter(
<Route path="form/useQueryStates" lazy={load(import('./routes/form.useQueryStates'))} />
<Route path="referential-stability/useQueryState" lazy={load(import('./routes/referential-stability.useQueryState'))} />
<Route path="referential-stability/useQueryStates" lazy={load(import('./routes/referential-stability.useQueryStates'))} />
{/* Reproductions */}
<Route path='repro-839' lazy={load(import('./routes/repro-839'))} />
</Route>
))

Expand Down
6 changes: 6 additions & 0 deletions packages/e2e/react-router/v6/src/routes/repro-839.tsx
Original file line number Diff line number Diff line change
@@ -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 <Repro839 useLocation={useLocation} useNavigate={useNavigate} />
}
5 changes: 4 additions & 1 deletion packages/e2e/react-router/v7/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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
6 changes: 6 additions & 0 deletions packages/e2e/react-router/v7/app/routes/repro-839.tsx
Original file line number Diff line number Diff line change
@@ -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 <Repro839 useLocation={useLocation} useNavigate={useNavigate} />
}
5 changes: 5 additions & 0 deletions packages/e2e/react-router/v7/cypress/e2e/repro-839.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { testRepro839LocationStatePersistence } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence.cy'

testRepro839LocationStatePersistence({
path: '/repro-839'
})
6 changes: 6 additions & 0 deletions packages/e2e/remix/app/routes/repro-839.tsx
Original file line number Diff line number Diff line change
@@ -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 <Repro839 useLocation={useLocation} useNavigate={useNavigate} />
}
5 changes: 5 additions & 0 deletions packages/e2e/remix/cypress/e2e/repro-839.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { testRepro839LocationStatePersistence } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence.cy'

testRepro839LocationStatePersistence({
path: '/repro-839'
})
Original file line number Diff line number Diff line change
@@ -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"}')
})
}
)
Original file line number Diff line number Diff line change
@@ -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 (
<>
<button
id="setup"
onClick={() => navigate('.', { state: { test: 'pass' } })}
>
Setup
</button>
<button id="shallow" onClick={() => setShallow('pass')}>
Test shallow
</button>
<button id="deep" onClick={() => setDeep('pass')}>
Test deep
</button>
<pre id="state">{JSON.stringify(location.state)}</pre>
</>
)
}
4 changes: 3 additions & 1 deletion packages/nuqs/src/adapters/lib/react-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type NavigateUrl = {
type NavigateOptions = {
replace?: boolean
preventScrollReset?: boolean
state?: unknown
}
type NavigateFn = (url: NavigateUrl, options: NavigateOptions) => void
type UseNavigate = () => NavigateFn
Expand Down Expand Up @@ -60,7 +61,8 @@ export function createReactRouterBasedAdapter(
},
{
replace: true,
preventScrollReset: true
preventScrollReset: true,
state: history.state?.usr
}
)
}
Expand Down

0 comments on commit 39d6f4a

Please sign in to comment.