Skip to content

Commit

Permalink
feat: Full-page navigation option for shallow: false updates in Rea…
Browse files Browse the repository at this point in the history
…ct SPA (#891)
  • Loading branch information
franky47 authored Feb 17, 2025
1 parent e11e88c commit 48cae6b
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 23 deletions.
11 changes: 8 additions & 3 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}

Expand Down
19 changes: 19 additions & 0 deletions packages/docs/content/docs/adapters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<NuqsAdapter fullPageNavigationOnShallowFalseUpdates>
<App />
</NuqsAdapter>
)
```

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"
Expand Down
6 changes: 5 additions & 1 deletion packages/e2e/react/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ enableHistorySync()

createRoot(document.getElementById('root')!).render(
<StrictMode>
<NuqsAdapter>
<NuqsAdapter
fullPageNavigationOnShallowFalseUpdates={
process.env.FULL_PAGE_NAV_ON_SHALLOW_FALSE === 'true'
}
>
<RootLayout>
<Router />
</RootLayout>
Expand Down
22 changes: 15 additions & 7 deletions packages/e2e/react/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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
)
}
}
}))
})
63 changes: 52 additions & 11 deletions packages/nuqs/src/adapters/react.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,48 @@
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'
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()
Expand All @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/**"],
Expand Down

0 comments on commit 48cae6b

Please sign in to comment.