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/**"],