From ef5b8ab7fd6b7c69c144bd1764747eb3301a13cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Best?= Date: Sat, 1 Feb 2025 22:11:06 +0100 Subject: [PATCH 1/5] feat: Allow `shallow: false` updates to reload the page in React SPA --- packages/nuqs/src/adapters/react.ts | 63 ++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/packages/nuqs/src/adapters/react.ts b/packages/nuqs/src/adapters/react.ts index 5c3f7437..02fb8c7d 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(reloadPageOnShallowFalseUpdates: boolean) { + return function updateUrl(search: URLSearchParams, options: AdapterOptions) { + const url = new URL(location.href) + url.search = renderQueryString(search) + if (reloadPageOnShallowFalseUpdates && 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({ + reloadPageOnShallowFalseUpdates: false +}) + function useNuqsReactAdapter() { + const { reloadPageOnShallowFalseUpdates } = 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(reloadPageOnShallowFalseUpdates), + [reloadPageOnShallowFalseUpdates] + ) return { searchParams, updateUrl } } -export const NuqsAdapter = createAdapterProvider(useNuqsReactAdapter) +const NuqsReactAdapter = createAdapterProvider(useNuqsReactAdapter) + +export function NuqsAdapter({ + children, + reloadPageOnShallowFalseUpdates = false +}: { + children: ReactNode + reloadPageOnShallowFalseUpdates?: boolean +}) { + return createElement( + NuqsReactAdapterContext.Provider, + { value: { reloadPageOnShallowFalseUpdates } }, + createElement(NuqsReactAdapter, null, children) + ) +} /** * Opt-in to syncing shallow updates of the URL with the useOptimisticSearchParams hook. From 59de8ee37e6b6165b882fd9b2e9f817c80bf4867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Best?= Date: Sun, 16 Feb 2025 23:06:56 +0100 Subject: [PATCH 2/5] test: Matrix on reloadPageOnShallowFalseUpdates true | false --- .github/workflows/ci-cd.yml | 11 ++++++++--- packages/e2e/react/src/main.tsx | 6 +++++- packages/e2e/react/vite.config.ts | 22 +++++++++++++++------- turbo.json | 6 +++++- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 0e71ebf2..f2d4e793 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-reload-${{ matrix.reload-on-shallow-false }}) runs-on: ubuntu-22.04-arm needs: [ci-core] + strategy: + fail-fast: false + matrix: + reload-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 }} + RELOAD_ON_SHALLOW_FALSE: ${{ matrix.reload-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-reload-${{ matrix.reload-on-shallow-false }} - uses: 47ng/actions-slack-notify@main name: Notify on Slack if: failure() with: status: ${{ job.status }} - jobName: react + jobName: react-reload-${{ matrix.reload-on-shallow-false }} env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/packages/e2e/react/src/main.tsx b/packages/e2e/react/src/main.tsx index 25fe54c2..8254ea7b 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..666dc33a 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.RELOAD_ON_SHALLOW_FALSE': JSON.stringify( + env.RELOAD_ON_SHALLOW_FALSE + ) + } } -})) +}) diff --git a/turbo.json b/turbo.json index c6a8f864..29e0e9d6 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": [ + "RELOAD_ON_SHALLOW_FALSE", + "REACT_COMPILER", + "E2E_NO_CACHE_ON_RERUN" + ] }, "docs#build": { "outputs": [".next/**", "!.next/cache/**"], From 3fd1a6ac5b1bd2d27d300e6529b8afb70be7d9fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Best?= Date: Mon, 17 Feb 2025 11:06:27 +0100 Subject: [PATCH 3/5] doc: Add reloadPageOnShallowFalseUpdates docs --- packages/docs/content/docs/adapters.mdx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/docs/content/docs/adapters.mdx b/packages/docs/content/docs/adapters.mdx index 0dd0b04a..693cf5a2 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, options +like [`shallow: false{:ts}`](./options#shallow) 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" /reloadPageOnShallowFalseUpdates/ +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" From 699078c91835d8b28bb37049d1a12214a295436c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Best?= Date: Mon, 17 Feb 2025 11:17:05 +0100 Subject: [PATCH 4/5] doc: Clarify wording on which option is a no-op --- packages/docs/content/docs/adapters.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docs/content/docs/adapters.mdx b/packages/docs/content/docs/adapters.mdx index 693cf5a2..f3e9c9c6 100644 --- a/packages/docs/content/docs/adapters.mdx +++ b/packages/docs/content/docs/adapters.mdx @@ -86,8 +86,8 @@ createRoot(document.getElementById('root')!).render( ) ``` -Note: because there is no known server in this configuration, options -like [`shallow: false{:ts}`](./options#shallow) will have no effect. +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 From c475b134bab018443bdeb468a0f6526dec5b3f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Best?= Date: Mon, 17 Feb 2025 11:21:36 +0100 Subject: [PATCH 5/5] ref: Better name for the option We're not doing a page reload, but a full page navigation. --- .github/workflows/ci-cd.yml | 10 +++++----- packages/docs/content/docs/adapters.mdx | 4 ++-- packages/e2e/react/src/main.tsx | 4 ++-- packages/e2e/react/vite.config.ts | 4 ++-- packages/nuqs/src/adapters/react.ts | 18 +++++++++--------- turbo.json | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index f2d4e793..0f8375e8 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -121,13 +121,13 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} e2e-react: - name: E2E (react-reload-${{ matrix.reload-on-shallow-false }}) + 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: - reload-on-shallow-false: [false, true] + full-page-nav-on-shallow-false: [false, true] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 @@ -143,19 +143,19 @@ jobs: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} E2E_NO_CACHE_ON_RERUN: ${{ github.run_attempt }} - RELOAD_ON_SHALLOW_FALSE: ${{ matrix.reload-on-shallow-false }} + 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-reload-${{ matrix.reload-on-shallow-false }} + 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-reload-${{ matrix.reload-on-shallow-false }} + 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 f3e9c9c6..67bf928e 100644 --- a/packages/docs/content/docs/adapters.mdx +++ b/packages/docs/content/docs/adapters.mdx @@ -94,9 +94,9 @@ updating query state configured with `shallow: false{:ts}`, to notify the web se 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" /reloadPageOnShallowFalseUpdates/ +```tsx title="src/main.tsx" /fullPageNavigationOnShallowFalseUpdates/ createRoot(document.getElementById('root')!).render( - + ) diff --git a/packages/e2e/react/src/main.tsx b/packages/e2e/react/src/main.tsx index 8254ea7b..11a222e0 100644 --- a/packages/e2e/react/src/main.tsx +++ b/packages/e2e/react/src/main.tsx @@ -9,8 +9,8 @@ enableHistorySync() createRoot(document.getElementById('root')!).render( diff --git a/packages/e2e/react/vite.config.ts b/packages/e2e/react/vite.config.ts index 666dc33a..14ae50e4 100644 --- a/packages/e2e/react/vite.config.ts +++ b/packages/e2e/react/vite.config.ts @@ -11,8 +11,8 @@ export default defineConfig(({ mode }) => { sourcemap: true }, define: { - 'process.env.RELOAD_ON_SHALLOW_FALSE': JSON.stringify( - env.RELOAD_ON_SHALLOW_FALSE + '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 02fb8c7d..6e92d1be 100644 --- a/packages/nuqs/src/adapters/react.ts +++ b/packages/nuqs/src/adapters/react.ts @@ -15,11 +15,11 @@ import { patchHistory, type SearchParamsSyncEmitter } from './lib/patch-history' const emitter: SearchParamsSyncEmitter = mitt() -function generateUpdateUrlFn(reloadPageOnShallowFalseUpdates: boolean) { +function generateUpdateUrlFn(fullPageNavigationOnShallowFalseUpdates: boolean) { return function updateUrl(search: URLSearchParams, options: AdapterOptions) { const url = new URL(location.href) url.search = renderQueryString(search) - if (reloadPageOnShallowFalseUpdates && options.shallow === false) { + if (fullPageNavigationOnShallowFalseUpdates && options.shallow === false) { const method = options.history === 'push' ? location.assign : location.replace method.call(location, url) @@ -36,11 +36,11 @@ function generateUpdateUrlFn(reloadPageOnShallowFalseUpdates: boolean) { } const NuqsReactAdapterContext = createContext({ - reloadPageOnShallowFalseUpdates: false + fullPageNavigationOnShallowFalseUpdates: false }) function useNuqsReactAdapter() { - const { reloadPageOnShallowFalseUpdates } = useContext( + const { fullPageNavigationOnShallowFalseUpdates } = useContext( NuqsReactAdapterContext ) const [searchParams, setSearchParams] = useState(() => { @@ -63,8 +63,8 @@ function useNuqsReactAdapter() { } }, []) const updateUrl = useMemo( - () => generateUpdateUrlFn(reloadPageOnShallowFalseUpdates), - [reloadPageOnShallowFalseUpdates] + () => generateUpdateUrlFn(fullPageNavigationOnShallowFalseUpdates), + [fullPageNavigationOnShallowFalseUpdates] ) return { searchParams, @@ -76,14 +76,14 @@ const NuqsReactAdapter = createAdapterProvider(useNuqsReactAdapter) export function NuqsAdapter({ children, - reloadPageOnShallowFalseUpdates = false + fullPageNavigationOnShallowFalseUpdates = false }: { children: ReactNode - reloadPageOnShallowFalseUpdates?: boolean + fullPageNavigationOnShallowFalseUpdates?: boolean }) { return createElement( NuqsReactAdapterContext.Provider, - { value: { reloadPageOnShallowFalseUpdates } }, + { value: { fullPageNavigationOnShallowFalseUpdates } }, createElement(NuqsReactAdapter, null, children) ) } diff --git a/turbo.json b/turbo.json index 29e0e9d6..53c9822b 100644 --- a/turbo.json +++ b/turbo.json @@ -35,7 +35,7 @@ "outputs": ["dist/**", "cypress/**"], "dependsOn": ["^build"], "env": [ - "RELOAD_ON_SHALLOW_FALSE", + "FULL_PAGE_NAV_ON_SHALLOW_FALSE", "REACT_COMPILER", "E2E_NO_CACHE_ON_RERUN" ]