Skip to content

Commit 48cae6b

Browse files
authored
feat: Full-page navigation option for shallow: false updates in React SPA (#891)
1 parent e11e88c commit 48cae6b

File tree

6 files changed

+104
-23
lines changed

6 files changed

+104
-23
lines changed

.github/workflows/ci-cd.yml

+8-3
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,13 @@ jobs:
121121
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
122122

123123
e2e-react:
124-
name: E2E (react)
124+
name: E2E (react-fpn-${{ matrix.full-page-nav-on-shallow-false }})
125125
runs-on: ubuntu-22.04-arm
126126
needs: [ci-core]
127+
strategy:
128+
fail-fast: false
129+
matrix:
130+
full-page-nav-on-shallow-false: [false, true]
127131
steps:
128132
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
129133
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
@@ -139,18 +143,19 @@ jobs:
139143
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
140144
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
141145
E2E_NO_CACHE_ON_RERUN: ${{ github.run_attempt }}
146+
FULL_PAGE_NAV_ON_SHALLOW_FALSE: ${{ matrix.full-page-nav-on-shallow-false }}
142147
- name: Save Cypress artifacts
143148
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08
144149
if: failure()
145150
with:
146151
path: packages/e2e/react/cypress/screenshots
147-
name: ci-react
152+
name: ci-react-fpn-${{ matrix.full-page-nav-on-shallow-false }}
148153
- uses: 47ng/actions-slack-notify@main
149154
name: Notify on Slack
150155
if: failure()
151156
with:
152157
status: ${{ job.status }}
153-
jobName: react
158+
jobName: react-fpn-${{ matrix.full-page-nav-on-shallow-false }}
154159
env:
155160
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
156161

packages/docs/content/docs/adapters.mdx

+19
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,25 @@ createRoot(document.getElementById('root')!).render(
8686
)
8787
```
8888

89+
Note: because there is no known server in this configuration, the
90+
[`shallow: false{:ts}`](./options#shallow) option will have no effect.
91+
92+
Since `nuqs@2.4.0`, you can specify a flag to perform a full-page navigation when
93+
updating query state configured with `shallow: false{:ts}`, to notify the web server
94+
that the URL state has changed, if it needs it for server-side rendering other
95+
parts of the application than the static React bundle:
96+
97+
```tsx title="src/main.tsx" /fullPageNavigationOnShallowFalseUpdates/
98+
createRoot(document.getElementById('root')!).render(
99+
<NuqsAdapter fullPageNavigationOnShallowFalseUpdates>
100+
<App />
101+
</NuqsAdapter>
102+
)
103+
```
104+
105+
This may be useful for servers not written in JavaScript, like Django (Python),
106+
Rails (Ruby), Laravel (PHP), Phoenix (Elixir) etc...
107+
89108
## Remix
90109

91110
```tsx title="app/root.tsx"

packages/e2e/react/src/main.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ enableHistorySync()
88

99
createRoot(document.getElementById('root')!).render(
1010
<StrictMode>
11-
<NuqsAdapter>
11+
<NuqsAdapter
12+
fullPageNavigationOnShallowFalseUpdates={
13+
process.env.FULL_PAGE_NAV_ON_SHALLOW_FALSE === 'true'
14+
}
15+
>
1216
<RootLayout>
1317
<Router />
1418
</RootLayout>

packages/e2e/react/vite.config.ts

+15-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import react from '@vitejs/plugin-react'
2-
import { defineConfig } from 'vite'
2+
import { defineConfig, loadEnv } from 'vite'
33

44
// https://vitejs.dev/config/
5-
export default defineConfig(() => ({
6-
plugins: [react()],
7-
build: {
8-
target: 'es2022',
9-
sourcemap: true
5+
export default defineConfig(({ mode }) => {
6+
const env = loadEnv(mode, process.cwd(), '')
7+
return {
8+
plugins: [react()],
9+
build: {
10+
target: 'es2022',
11+
sourcemap: true
12+
},
13+
define: {
14+
'process.env.FULL_PAGE_NAV_ON_SHALLOW_FALSE': JSON.stringify(
15+
env.FULL_PAGE_NAV_ON_SHALLOW_FALSE
16+
)
17+
}
1018
}
11-
}))
19+
})

packages/nuqs/src/adapters/react.ts

+52-11
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,48 @@
11
import mitt from 'mitt'
2-
import { useEffect, useState } from 'react'
2+
import {
3+
createContext,
4+
createElement,
5+
useContext,
6+
useEffect,
7+
useMemo,
8+
useState,
9+
type ReactNode
10+
} from 'react'
311
import { renderQueryString } from '../url-encoding'
412
import { createAdapterProvider } from './lib/context'
513
import type { AdapterOptions } from './lib/defs'
614
import { patchHistory, type SearchParamsSyncEmitter } from './lib/patch-history'
715

816
const emitter: SearchParamsSyncEmitter = mitt()
917

10-
function updateUrl(search: URLSearchParams, options: AdapterOptions) {
11-
const url = new URL(location.href)
12-
url.search = renderQueryString(search)
13-
const method =
14-
options.history === 'push' ? history.pushState : history.replaceState
15-
method.call(history, history.state, '', url)
16-
emitter.emit('update', search)
17-
if (options.scroll === true) {
18-
window.scrollTo({ top: 0 })
18+
function generateUpdateUrlFn(fullPageNavigationOnShallowFalseUpdates: boolean) {
19+
return function updateUrl(search: URLSearchParams, options: AdapterOptions) {
20+
const url = new URL(location.href)
21+
url.search = renderQueryString(search)
22+
if (fullPageNavigationOnShallowFalseUpdates && options.shallow === false) {
23+
const method =
24+
options.history === 'push' ? location.assign : location.replace
25+
method.call(location, url)
26+
} else {
27+
const method =
28+
options.history === 'push' ? history.pushState : history.replaceState
29+
method.call(history, history.state, '', url)
30+
}
31+
emitter.emit('update', search)
32+
if (options.scroll === true) {
33+
window.scrollTo({ top: 0 })
34+
}
1935
}
2036
}
2137

38+
const NuqsReactAdapterContext = createContext({
39+
fullPageNavigationOnShallowFalseUpdates: false
40+
})
41+
2242
function useNuqsReactAdapter() {
43+
const { fullPageNavigationOnShallowFalseUpdates } = useContext(
44+
NuqsReactAdapterContext
45+
)
2346
const [searchParams, setSearchParams] = useState(() => {
2447
if (typeof location === 'undefined') {
2548
return new URLSearchParams()
@@ -39,13 +62,31 @@ function useNuqsReactAdapter() {
3962
window.removeEventListener('popstate', onPopState)
4063
}
4164
}, [])
65+
const updateUrl = useMemo(
66+
() => generateUpdateUrlFn(fullPageNavigationOnShallowFalseUpdates),
67+
[fullPageNavigationOnShallowFalseUpdates]
68+
)
4269
return {
4370
searchParams,
4471
updateUrl
4572
}
4673
}
4774

48-
export const NuqsAdapter = createAdapterProvider(useNuqsReactAdapter)
75+
const NuqsReactAdapter = createAdapterProvider(useNuqsReactAdapter)
76+
77+
export function NuqsAdapter({
78+
children,
79+
fullPageNavigationOnShallowFalseUpdates = false
80+
}: {
81+
children: ReactNode
82+
fullPageNavigationOnShallowFalseUpdates?: boolean
83+
}) {
84+
return createElement(
85+
NuqsReactAdapterContext.Provider,
86+
{ value: { fullPageNavigationOnShallowFalseUpdates } },
87+
createElement(NuqsReactAdapter, null, children)
88+
)
89+
}
4990

5091
/**
5192
* Opt-in to syncing shallow updates of the URL with the useOptimisticSearchParams hook.

turbo.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@
3434
"e2e-react#build": {
3535
"outputs": ["dist/**", "cypress/**"],
3636
"dependsOn": ["^build"],
37-
"env": ["REACT_COMPILER", "E2E_NO_CACHE_ON_RERUN"]
37+
"env": [
38+
"FULL_PAGE_NAV_ON_SHALLOW_FALSE",
39+
"REACT_COMPILER",
40+
"E2E_NO_CACHE_ON_RERUN"
41+
]
3842
},
3943
"docs#build": {
4044
"outputs": [".next/**", "!.next/cache/**"],

0 commit comments

Comments
 (0)