Skip to content

Commit 39d6f4a

Browse files
authored
fix: Maintain location.state in React Router frameworks (#840)
* test: Add failing test for issue 839 * fix: Forward user-defined location.state * doc: Add disclaimer about router support
1 parent c089be2 commit 39d6f4a

File tree

12 files changed

+110
-2
lines changed

12 files changed

+110
-2
lines changed

packages/docs/content/docs/adapters.mdx

+11
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ export function ReactRouter() {
127127
}
128128
```
129129

130+
<Callout>
131+
132+
Only `BrowserRouter` is supported. There may be support for `HashRouter`
133+
in the future (see issue [#810](https://github.com/47ng/nuqs/issues/810)), but
134+
support for `MemoryRouter` is not planned.
135+
136+
</Callout>
137+
130138
## React Router v7
131139

132140
```tsx title="app/root.tsx"
@@ -151,6 +159,9 @@ export default function App() {
151159

152160
Please pin your imports to the specific version,
153161
eg: `nuqs/adapters/react-router/v6` or `nuqs/adapters/react-router/v7`.
162+
163+
The main difference is where the React Router hooks are imported from:
164+
`react-router-dom` for v6, and `react-router` for v7.
154165
</Callout>
155166

156167
## Testing
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { testRepro839LocationStatePersistence } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence.cy'
2+
3+
testRepro839LocationStatePersistence({
4+
path: '/repro-839'
5+
})

packages/e2e/react-router/v6/src/react-router.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ function load(mod: Promise<{ default: any; [otherExports: string]: any }>) {
2020
const router = createBrowserRouter(
2121
createRoutesFromElements(
2222
<Route path="/" element={<RootLayout/>} >,
23+
{/* Shared E2E tests */}
2324
<Route path='hash-preservation' lazy={load(import('./routes/hash-preservation'))} />
2425
<Route path='basic-io/useQueryState' lazy={load(import('./routes/basic-io.useQueryState'))} />
2526
<Route path='basic-io/useQueryStates' lazy={load(import('./routes/basic-io.useQueryStates'))} />
@@ -40,6 +41,8 @@ const router = createBrowserRouter(
4041
<Route path="form/useQueryStates" lazy={load(import('./routes/form.useQueryStates'))} />
4142
<Route path="referential-stability/useQueryState" lazy={load(import('./routes/referential-stability.useQueryState'))} />
4243
<Route path="referential-stability/useQueryStates" lazy={load(import('./routes/referential-stability.useQueryStates'))} />
44+
{/* Reproductions */}
45+
<Route path='repro-839' lazy={load(import('./routes/repro-839'))} />
4346
</Route>
4447
))
4548

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Repro839 } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence'
2+
import { useLocation, useNavigate } from 'react-router-dom'
3+
4+
export default function Page() {
5+
return <Repro839 useLocation={useLocation} useNavigate={useNavigate} />
6+
}

packages/e2e/react-router/v7/app/routes.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type RouteConfig, layout, route } from '@react-router/dev/routes'
33
export default [
44
// prettier-ignore
55
layout('layout.tsx', [
6+
// Shared E2E tests
67
route('/hash-preservation', './routes/hash-preservation.tsx'),
78
route('/basic-io/useQueryState', './routes/basic-io.useQueryState.tsx'),
89
route('/basic-io/useQueryStates', './routes/basic-io.useQueryStates.tsx'),
@@ -22,6 +23,8 @@ export default [
2223
route('/form/useQueryState', './routes/form.useQueryState.tsx'),
2324
route('/form/useQueryStates', './routes/form.useQueryStates.tsx'),
2425
route('/referential-stability/useQueryState', './routes/referential-stability.useQueryState.tsx'),
25-
route('/referential-stability/useQueryStates', './routes/referential-stability.useQueryStates.tsx')
26+
route('/referential-stability/useQueryStates', './routes/referential-stability.useQueryStates.tsx'),
27+
// Reproductions
28+
route('/repro-839', './routes/repro-839.tsx'),
2629
])
2730
] satisfies RouteConfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Repro839 } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence'
2+
import { useLocation, useNavigate } from 'react-router'
3+
4+
export default function Page() {
5+
return <Repro839 useLocation={useLocation} useNavigate={useNavigate} />
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { testRepro839LocationStatePersistence } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence.cy'
2+
3+
testRepro839LocationStatePersistence({
4+
path: '/repro-839'
5+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { useLocation, useNavigate } from '@remix-run/react'
2+
import { Repro839 } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence'
3+
4+
export default function Page() {
5+
return <Repro839 useLocation={useLocation} useNavigate={useNavigate} />
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { testRepro839LocationStatePersistence } from 'e2e-shared/specs/react-router/repro-839-location-state-persistence.cy'
2+
3+
testRepro839LocationStatePersistence({
4+
path: '/repro-839'
5+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createTest } from '../../create-test'
2+
3+
export const testRepro839LocationStatePersistence = createTest(
4+
'Repro for issue #839 - Location state persistence',
5+
({ path }) => {
6+
it('persists location.state on shallow URL updates', () => {
7+
cy.visit(path)
8+
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
9+
cy.get('#setup').click()
10+
cy.get('#shallow').click()
11+
cy.get('#state').should('have.text', '{"test":"pass"}')
12+
})
13+
14+
it('persists location.state on deep URL updates', () => {
15+
cy.visit(path)
16+
cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
17+
cy.get('#setup').click()
18+
cy.get('#deep').click()
19+
cy.get('#state').should('have.text', '{"test":"pass"}')
20+
})
21+
}
22+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useQueryState } from 'nuqs'
2+
3+
type Repro839Props = {
4+
useNavigate: () => (url: string, options: { state: unknown }) => void
5+
useLocation: () => { state: unknown }
6+
}
7+
8+
export function Repro839({ useNavigate, useLocation }: Repro839Props) {
9+
const navigate = useNavigate()
10+
const location = useLocation()
11+
const [, setShallow] = useQueryState('shallow', {
12+
shallow: true
13+
})
14+
const [, setDeep] = useQueryState('deep', {
15+
shallow: false
16+
})
17+
return (
18+
<>
19+
<button
20+
id="setup"
21+
onClick={() => navigate('.', { state: { test: 'pass' } })}
22+
>
23+
Setup
24+
</button>
25+
<button id="shallow" onClick={() => setShallow('pass')}>
26+
Test shallow
27+
</button>
28+
<button id="deep" onClick={() => setDeep('pass')}>
29+
Test deep
30+
</button>
31+
<pre id="state">{JSON.stringify(location.state)}</pre>
32+
</>
33+
)
34+
}

packages/nuqs/src/adapters/lib/react-router.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type NavigateUrl = {
1616
type NavigateOptions = {
1717
replace?: boolean
1818
preventScrollReset?: boolean
19+
state?: unknown
1920
}
2021
type NavigateFn = (url: NavigateUrl, options: NavigateOptions) => void
2122
type UseNavigate = () => NavigateFn
@@ -60,7 +61,8 @@ export function createReactRouterBasedAdapter(
6061
},
6162
{
6263
replace: true,
63-
preventScrollReset: true
64+
preventScrollReset: true,
65+
state: history.state?.usr
6466
}
6567
)
6668
}

0 commit comments

Comments
 (0)