diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 0e71ebf2..0f8375e8 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -121,9 +121,13 @@ jobs:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
e2e-react:
- name: E2E (react)
+ name: E2E (react-fpn-${{ matrix.full-page-nav-on-shallow-false }})
runs-on: ubuntu-22.04-arm
needs: [ci-core]
+ strategy:
+ fail-fast: false
+ matrix:
+ full-page-nav-on-shallow-false: [false, true]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
@@ -139,18 +143,19 @@ jobs:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
E2E_NO_CACHE_ON_RERUN: ${{ github.run_attempt }}
+ FULL_PAGE_NAV_ON_SHALLOW_FALSE: ${{ matrix.full-page-nav-on-shallow-false }}
- name: Save Cypress artifacts
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08
if: failure()
with:
path: packages/e2e/react/cypress/screenshots
- name: ci-react
+ name: ci-react-fpn-${{ matrix.full-page-nav-on-shallow-false }}
- uses: 47ng/actions-slack-notify@main
name: Notify on Slack
if: failure()
with:
status: ${{ job.status }}
- jobName: react
+ jobName: react-fpn-${{ matrix.full-page-nav-on-shallow-false }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
diff --git a/packages/docs/content/docs/adapters.mdx b/packages/docs/content/docs/adapters.mdx
index 0dd0b04a..67bf928e 100644
--- a/packages/docs/content/docs/adapters.mdx
+++ b/packages/docs/content/docs/adapters.mdx
@@ -86,6 +86,25 @@ createRoot(document.getElementById('root')!).render(
)
```
+Note: because there is no known server in this configuration, the
+[`shallow: false{:ts}`](./options#shallow) option will have no effect.
+
+Since `nuqs@2.4.0`, you can specify a flag to perform a full-page navigation when
+updating query state configured with `shallow: false{:ts}`, to notify the web server
+that the URL state has changed, if it needs it for server-side rendering other
+parts of the application than the static React bundle:
+
+```tsx title="src/main.tsx" /fullPageNavigationOnShallowFalseUpdates/
+createRoot(document.getElementById('root')!).render(
+
+
+
+)
+```
+
+This may be useful for servers not written in JavaScript, like Django (Python),
+Rails (Ruby), Laravel (PHP), Phoenix (Elixir) etc...
+
## Remix
```tsx title="app/root.tsx"
diff --git a/packages/e2e/react/src/main.tsx b/packages/e2e/react/src/main.tsx
index 25fe54c2..11a222e0 100644
--- a/packages/e2e/react/src/main.tsx
+++ b/packages/e2e/react/src/main.tsx
@@ -8,7 +8,11 @@ enableHistorySync()
createRoot(document.getElementById('root')!).render(
-
+
diff --git a/packages/e2e/react/vite.config.ts b/packages/e2e/react/vite.config.ts
index 1a6afb12..14ae50e4 100644
--- a/packages/e2e/react/vite.config.ts
+++ b/packages/e2e/react/vite.config.ts
@@ -1,11 +1,19 @@
import react from '@vitejs/plugin-react'
-import { defineConfig } from 'vite'
+import { defineConfig, loadEnv } from 'vite'
// https://vitejs.dev/config/
-export default defineConfig(() => ({
- plugins: [react()],
- build: {
- target: 'es2022',
- sourcemap: true
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, process.cwd(), '')
+ return {
+ plugins: [react()],
+ build: {
+ target: 'es2022',
+ sourcemap: true
+ },
+ define: {
+ 'process.env.FULL_PAGE_NAV_ON_SHALLOW_FALSE': JSON.stringify(
+ env.FULL_PAGE_NAV_ON_SHALLOW_FALSE
+ )
+ }
}
-}))
+})
diff --git a/packages/nuqs/src/adapters/react.ts b/packages/nuqs/src/adapters/react.ts
index 5c3f7437..6e92d1be 100644
--- a/packages/nuqs/src/adapters/react.ts
+++ b/packages/nuqs/src/adapters/react.ts
@@ -1,5 +1,13 @@
import mitt from 'mitt'
-import { useEffect, useState } from 'react'
+import {
+ createContext,
+ createElement,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ type ReactNode
+} from 'react'
import { renderQueryString } from '../url-encoding'
import { createAdapterProvider } from './lib/context'
import type { AdapterOptions } from './lib/defs'
@@ -7,19 +15,34 @@ import { patchHistory, type SearchParamsSyncEmitter } from './lib/patch-history'
const emitter: SearchParamsSyncEmitter = mitt()
-function updateUrl(search: URLSearchParams, options: AdapterOptions) {
- const url = new URL(location.href)
- url.search = renderQueryString(search)
- const method =
- options.history === 'push' ? history.pushState : history.replaceState
- method.call(history, history.state, '', url)
- emitter.emit('update', search)
- if (options.scroll === true) {
- window.scrollTo({ top: 0 })
+function generateUpdateUrlFn(fullPageNavigationOnShallowFalseUpdates: boolean) {
+ return function updateUrl(search: URLSearchParams, options: AdapterOptions) {
+ const url = new URL(location.href)
+ url.search = renderQueryString(search)
+ if (fullPageNavigationOnShallowFalseUpdates && options.shallow === false) {
+ const method =
+ options.history === 'push' ? location.assign : location.replace
+ method.call(location, url)
+ } else {
+ const method =
+ options.history === 'push' ? history.pushState : history.replaceState
+ method.call(history, history.state, '', url)
+ }
+ emitter.emit('update', search)
+ if (options.scroll === true) {
+ window.scrollTo({ top: 0 })
+ }
}
}
+const NuqsReactAdapterContext = createContext({
+ fullPageNavigationOnShallowFalseUpdates: false
+})
+
function useNuqsReactAdapter() {
+ const { fullPageNavigationOnShallowFalseUpdates } = useContext(
+ NuqsReactAdapterContext
+ )
const [searchParams, setSearchParams] = useState(() => {
if (typeof location === 'undefined') {
return new URLSearchParams()
@@ -39,13 +62,31 @@ function useNuqsReactAdapter() {
window.removeEventListener('popstate', onPopState)
}
}, [])
+ const updateUrl = useMemo(
+ () => generateUpdateUrlFn(fullPageNavigationOnShallowFalseUpdates),
+ [fullPageNavigationOnShallowFalseUpdates]
+ )
return {
searchParams,
updateUrl
}
}
-export const NuqsAdapter = createAdapterProvider(useNuqsReactAdapter)
+const NuqsReactAdapter = createAdapterProvider(useNuqsReactAdapter)
+
+export function NuqsAdapter({
+ children,
+ fullPageNavigationOnShallowFalseUpdates = false
+}: {
+ children: ReactNode
+ fullPageNavigationOnShallowFalseUpdates?: boolean
+}) {
+ return createElement(
+ NuqsReactAdapterContext.Provider,
+ { value: { fullPageNavigationOnShallowFalseUpdates } },
+ createElement(NuqsReactAdapter, null, children)
+ )
+}
/**
* Opt-in to syncing shallow updates of the URL with the useOptimisticSearchParams hook.
diff --git a/turbo.json b/turbo.json
index c6a8f864..53c9822b 100644
--- a/turbo.json
+++ b/turbo.json
@@ -34,7 +34,11 @@
"e2e-react#build": {
"outputs": ["dist/**", "cypress/**"],
"dependsOn": ["^build"],
- "env": ["REACT_COMPILER", "E2E_NO_CACHE_ON_RERUN"]
+ "env": [
+ "FULL_PAGE_NAV_ON_SHALLOW_FALSE",
+ "REACT_COMPILER",
+ "E2E_NO_CACHE_ON_RERUN"
+ ]
},
"docs#build": {
"outputs": [".next/**", "!.next/cache/**"],