diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index c6195b98..5adde0ec 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -10,8 +10,19 @@ env: jobs: ci: - name: Integration + name: Integration (Next.js ${{ matrix.next-version }}${{ matrix.base-path != '/' && ' with basePath' || ''}}) runs-on: ubuntu-latest + strategy: + matrix: + base-path: ['/'] + next-version: + - '13.4' + - '13.5' + - '14' + - latest + include: + - next-version: 'latest' + base-path: '/base' steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - uses: pnpm/action-setup@d882d12c64e032187b2edb46d3a0d003b7a43598 @@ -23,19 +34,20 @@ jobs: cache: pnpm - name: Install dependencies run: pnpm install + - name: Install Next.js version ${{ matrix.next-version }} + run: pnpm add --filter e2e next@${{ matrix.next-version }} - name: Run integration tests - run: pnpm run ci + run: pnpm run test env: + BASE_PATH: ${{ matrix.base-path }} TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - # - uses: coverallsapp/github-action@3dfc5567390f6fa9267c0ee9c251e4c8c3f18949 - # name: Report code coverage - # continue-on-error: true - uses: 47ng/actions-slack-notify@main name: Notify on Slack if: always() with: status: ${{ job.status }} + jobName: next@${{ matrix.next-version }} env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} @@ -65,4 +77,3 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - SCEAU_PRIVATE_KEY: ${{ secrets.SCEAU_PRIVATE_KEY }} diff --git a/.github/workflows/test-against-nextjs-release.yml b/.github/workflows/test-against-nextjs-release.yml index de16c4e4..67c90b93 100644 --- a/.github/workflows/test-against-nextjs-release.yml +++ b/.github/workflows/test-against-nextjs-release.yml @@ -28,9 +28,9 @@ jobs: - name: Install dependencies run: pnpm install - name: Install Next.js version ${{ inputs.version }} - run: pnpm add --filter next-usequerystate-playground next@${{ inputs.version }} + run: pnpm add --filter e2e next@${{ inputs.version }} - name: Run integration tests - run: pnpm run ci + run: pnpm run test env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f04b10c..13b5e107 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,21 +13,21 @@ First off, thanks for your help! This monorepo contains: - The source code for the `next-usequerystate` NPM package, in [`packages/next-usequerystate`](./packages/next-usequerystate). -- A Next.js app under [`packages/playground`](./packages/playground) that serves as: - - A playground deployed at - - A host for [end-to-end tests](./packages/playground/cypress//e2e) driven by Cypress +- A Next.js app under [`packages/playground`](./packages/playground) that serves as a playground deployed at +- A test bench for [end-to-end tests](./packages/e2e) driven by Cypress When running `next dev`, this will: - Build the library and watch for changes using [`tsup`](https://tsup.egoist.dev/) - Start the playground, which will be available at . +- Start the end-to-end test bench, which will be available at . ## Testing -You can run the complete integration test suite with `pnpm run ci`. +You can run the complete integration test suite with `pnpm test`. It will build the library, run unit tests and typing tests against it, and then -run the end-to-end tests against the playground (which uses the built library). +run the end-to-end tests against the test bench Next.js app (which uses the built library). When proposing changes or showcasing a bug, adding a minimal reproduction in the playground can be very helpful. diff --git a/package.json b/package.json index dfe5f280..04a3f1fb 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "dev": "FORCE_COLOR=3 turbo run dev", "build": "FORCE_COLOR=3 turbo run build", "test": "FORCE_COLOR=3 turbo run test", - "ci": "FORCE_COLOR=3 turbo run build test", "prepare": "husky install" }, "devDependencies": { diff --git a/packages/playground/cypress.config.ts b/packages/e2e/cypress.config.ts similarity index 60% rename from packages/playground/cypress.config.ts rename to packages/e2e/cypress.config.ts index 39a13284..b9ac1c4b 100644 --- a/packages/playground/cypress.config.ts +++ b/packages/e2e/cypress.config.ts @@ -1,8 +1,10 @@ import { defineConfig } from 'cypress' +const basePath = process.env.BASE_PATH === '/' ? '' : process.env.BASE_PATH + export default defineConfig({ e2e: { - baseUrl: `http://localhost:3000`, + baseUrl: `http://localhost:3001${basePath}`, video: false, fixturesFolder: false, supportFile: false, diff --git a/packages/playground/cypress/.gitignore b/packages/e2e/cypress/.gitignore similarity index 100% rename from packages/playground/cypress/.gitignore rename to packages/e2e/cypress/.gitignore diff --git a/packages/playground/cypress/e2e/repro-388.cy.js b/packages/e2e/cypress/e2e/repro-388.cy.js similarity index 94% rename from packages/playground/cypress/e2e/repro-388.cy.js rename to packages/e2e/cypress/e2e/repro-388.cy.js index 394030dc..57a25540 100644 --- a/packages/playground/cypress/e2e/repro-388.cy.js +++ b/packages/e2e/cypress/e2e/repro-388.cy.js @@ -2,7 +2,7 @@ it('Reproduction for issue #388', () => { cy.config('retries', 0) - cy.visit('/e2e/app/repro-388') + cy.visit('/app/repro-388') cy.contains('#hydration-marker', 'hydrated').should('be.hidden') cy.get('#start').click() @@ -18,7 +18,7 @@ it('Reproduction for issue #388', () => { cy.get('#counter').should('have.text', 'Counter: 1') // Reset the page - cy.visit('/e2e/app/repro-388') + cy.visit('/app/repro-388') cy.get('#start').click() // The URL should have a ?counter=1 query string cy.location('search').should('eq', '?counter=1') diff --git a/packages/playground/cypress/e2e/useQueryState.cy.js b/packages/e2e/cypress/e2e/useQueryState.cy.js similarity index 94% rename from packages/playground/cypress/e2e/useQueryState.cy.js rename to packages/e2e/cypress/e2e/useQueryState.cy.js index b0a2390b..c3090513 100644 --- a/packages/playground/cypress/e2e/useQueryState.cy.js +++ b/packages/e2e/cypress/e2e/useQueryState.cy.js @@ -87,24 +87,24 @@ function runTest() { describe('useQueryState (app router)', () => { it('works in standard routes', () => { - cy.visit('/e2e/app/useQueryState') + cy.visit('/app/useQueryState') runTest() }) it('works in dynamic routes', () => { - cy.visit('/e2e/app/useQueryState/dynamic/route') + cy.visit('/app/useQueryState/dynamic/route') runTest() }) }) describe('useQueryState (pages router)', () => { it('works in standard routes', () => { - cy.visit('/e2e/pages/useQueryState') + cy.visit('/pages/useQueryState') runTest() }) it('works in dynamic routes', () => { - cy.visit('/e2e/pages/useQueryState/dynamic/route') + cy.visit('/pages/useQueryState/dynamic/route') runTest() }) }) diff --git a/packages/playground/cypress/e2e/useQueryStates.cy.js b/packages/e2e/cypress/e2e/useQueryStates.cy.js similarity index 93% rename from packages/playground/cypress/e2e/useQueryStates.cy.js rename to packages/e2e/cypress/e2e/useQueryStates.cy.js index 4aafc73e..36edff35 100644 --- a/packages/playground/cypress/e2e/useQueryStates.cy.js +++ b/packages/e2e/cypress/e2e/useQueryStates.cy.js @@ -77,24 +77,24 @@ function runTest() { describe('useQueryStates (app router)', () => { it('uses string by default', () => { - cy.visit('/e2e/app/useQueryStates') + cy.visit('/app/useQueryStates') runTest() }) it('should work with dynamic routes', () => { - cy.visit('/e2e/app/useQueryStates/dynamic/route') + cy.visit('/app/useQueryStates/dynamic/route') runTest() }) }) describe('useQueryStates (pages router)', () => { it('uses string by default', () => { - cy.visit('/e2e/pages/useQueryStates') + cy.visit('/pages/useQueryStates') runTest() }) it('should work with dynamic routes', () => { - cy.visit('/e2e/pages/useQueryStates/dynamic/route') + cy.visit('/pages/useQueryStates/dynamic/route') runTest() }) }) diff --git a/packages/e2e/next-env.d.ts b/packages/e2e/next-env.d.ts new file mode 100644 index 00000000..fd36f949 --- /dev/null +++ b/packages/e2e/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/packages/e2e/next.config.mjs b/packages/e2e/next.config.mjs new file mode 100644 index 00000000..9e0699e2 --- /dev/null +++ b/packages/e2e/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig } */ +const config = { + basePath: process.env.BASE_PATH === '/' ? undefined : process.env.BASE_PATH +} + +export default config diff --git a/packages/e2e/package.json b/packages/e2e/package.json new file mode 100644 index 00000000..19ca0bb5 --- /dev/null +++ b/packages/e2e/package.json @@ -0,0 +1,37 @@ +{ + "name": "e2e", + "version": "0.0.0-internal", + "description": "End-to-end test bench for next-usequerystate", + "license": "MIT", + "private": true, + "author": { + "name": "François Best", + "email": "contact@francoisbest.com", + "url": "https://francoisbest.com" + }, + "type": "module", + "scripts": { + "dev": "next dev --port 3001", + "build": "next build", + "start": "next start --port 3001", + "pretest": "cypress install", + "test": "run-p --race start cypress:run", + "cypress:open": "cypress open", + "cypress:run": "cypress run --headless" + }, + "dependencies": { + "next": "14.0.2-canary.7", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "next-usequerystate": "workspace:*" + }, + "devDependencies": { + "@types/node": "^20.8.9", + "@types/react": "^18.2.33", + "@types/react-dom": "^18.2.14", + "@types/webpack": "^5.28.4", + "cypress": "^13.3.3", + "npm-run-all": "^4.1.5", + "typescript": "^5.2.2" + } +} diff --git a/packages/playground/src/app/e2e/app/repro-388/page.tsx b/packages/e2e/src/app/app/repro-388/page.tsx similarity index 78% rename from packages/playground/src/app/e2e/app/repro-388/page.tsx rename to packages/e2e/src/app/app/repro-388/page.tsx index 139af845..61d2d6cd 100644 --- a/packages/playground/src/app/e2e/app/repro-388/page.tsx +++ b/packages/e2e/src/app/app/repro-388/page.tsx @@ -1,21 +1,30 @@ 'use client' import { parseAsInteger, useQueryState } from 'next-usequerystate' +import { PrefetchKind } from 'next/dist/client/components/router-reducer/router-reducer-types' import Link from 'next/link' +import { useRouter } from 'next/navigation' import React from 'react' export default function Page() { + const router = useRouter() const [counter, setCounter] = useQueryState( 'counter', parseAsInteger.withDefault(0) ) const [mounted, setMounted] = React.useState(false) + const manualPrefetch = React.useCallback(() => { + router.prefetch('/', { kind: PrefetchKind.FULL }) + }, [router]) return ( <> + <>

The counter is set but only in the History API, the Next.js router @@ -42,7 +51,7 @@ export default function Page() { {mounted && ( = ({ return ( <> diff --git a/packages/playground/src/app/e2e/app/routing-tour/a/page.tsx b/packages/e2e/src/app/app/routing-tour/a/page.tsx similarity index 100% rename from packages/playground/src/app/e2e/app/routing-tour/a/page.tsx rename to packages/e2e/src/app/app/routing-tour/a/page.tsx diff --git a/packages/playground/src/app/e2e/app/routing-tour/b/page.tsx b/packages/e2e/src/app/app/routing-tour/b/page.tsx similarity index 100% rename from packages/playground/src/app/e2e/app/routing-tour/b/page.tsx rename to packages/e2e/src/app/app/routing-tour/b/page.tsx diff --git a/packages/playground/src/app/e2e/app/routing-tour/c/page.tsx b/packages/e2e/src/app/app/routing-tour/c/page.tsx similarity index 100% rename from packages/playground/src/app/e2e/app/routing-tour/c/page.tsx rename to packages/e2e/src/app/app/routing-tour/c/page.tsx diff --git a/packages/playground/src/app/e2e/app/routing-tour/d/page.tsx b/packages/e2e/src/app/app/routing-tour/d/page.tsx similarity index 100% rename from packages/playground/src/app/e2e/app/routing-tour/d/page.tsx rename to packages/e2e/src/app/app/routing-tour/d/page.tsx diff --git a/packages/playground/src/app/e2e/app/routing-tour/start/client/page.tsx b/packages/e2e/src/app/app/routing-tour/start/client/page.tsx similarity index 58% rename from packages/playground/src/app/e2e/app/routing-tour/start/client/page.tsx rename to packages/e2e/src/app/app/routing-tour/start/client/page.tsx index 646439cb..597bf816 100644 --- a/packages/playground/src/app/e2e/app/routing-tour/start/client/page.tsx +++ b/packages/e2e/src/app/app/routing-tour/start/client/page.tsx @@ -6,22 +6,22 @@ export default function ServerStartPage() { return (

  • - + a (server, prefetch)
  • - + b (server, no prefetch)
  • - + c (client, prefetch)
  • - + d (client, no prefetch)
  • diff --git a/packages/playground/src/app/e2e/app/routing-tour/start/server/page.tsx b/packages/e2e/src/app/app/routing-tour/start/server/page.tsx similarity index 57% rename from packages/playground/src/app/e2e/app/routing-tour/start/server/page.tsx rename to packages/e2e/src/app/app/routing-tour/start/server/page.tsx index b618eb37..8c728f25 100644 --- a/packages/playground/src/app/e2e/app/routing-tour/start/server/page.tsx +++ b/packages/e2e/src/app/app/routing-tour/start/server/page.tsx @@ -4,22 +4,22 @@ export default function ServerStartPage() { return (
    • - + a (server, prefetch)
    • - + b (server, no prefetch)
    • - + c (client, prefetch)
    • - + d (client, no prefetch)
    • diff --git a/packages/playground/src/app/e2e/app/useQueryState/dynamic/[route]/page.tsx b/packages/e2e/src/app/app/useQueryState/dynamic/[route]/page.tsx similarity index 100% rename from packages/playground/src/app/e2e/app/useQueryState/dynamic/[route]/page.tsx rename to packages/e2e/src/app/app/useQueryState/dynamic/[route]/page.tsx diff --git a/packages/playground/src/app/e2e/app/useQueryState/page.tsx b/packages/e2e/src/app/app/useQueryState/page.tsx similarity index 100% rename from packages/playground/src/app/e2e/app/useQueryState/page.tsx rename to packages/e2e/src/app/app/useQueryState/page.tsx diff --git a/packages/playground/src/app/e2e/app/useQueryStates/dynamic/[route]/page.tsx b/packages/e2e/src/app/app/useQueryStates/dynamic/[route]/page.tsx similarity index 100% rename from packages/playground/src/app/e2e/app/useQueryStates/dynamic/[route]/page.tsx rename to packages/e2e/src/app/app/useQueryStates/dynamic/[route]/page.tsx diff --git a/packages/playground/src/app/e2e/app/useQueryStates/page.tsx b/packages/e2e/src/app/app/useQueryStates/page.tsx similarity index 100% rename from packages/playground/src/app/e2e/app/useQueryStates/page.tsx rename to packages/e2e/src/app/app/useQueryStates/page.tsx diff --git a/packages/e2e/src/app/layout.tsx b/packages/e2e/src/app/layout.tsx new file mode 100644 index 00000000..d943ffd5 --- /dev/null +++ b/packages/e2e/src/app/layout.tsx @@ -0,0 +1,25 @@ +import React, { Suspense } from 'react' +import { HydrationMarker } from '../components/hydration-marker' + +export const metadata = { + title: 'next-usequerystate playground', + description: + 'useQueryState hook for Next.js - Like React.useState, but stored in the URL query string' +} + +export default function RootLayout({ + children +}: { + children: React.ReactNode +}) { + return ( + + + + + + {children} + + + ) +} diff --git a/packages/e2e/src/app/page.tsx b/packages/e2e/src/app/page.tsx new file mode 100644 index 00000000..eac52dbf --- /dev/null +++ b/packages/e2e/src/app/page.tsx @@ -0,0 +1,58 @@ +import Link from 'next/link' + +export default function IndexPage() { + return ( +
      +

      End-to-end integration tests

      +

      ⚠️ Don't change these routes without updating integration tests.

      +

      App router

      +
        +
      • + [static] useQueryState +
      • +
      • + + [dynamic] useQueryState + +
      • +
      • + [static] useQueryStates +
      • +
      • + + [dynamic] useQueryStates + +
      • +
      • + + Routing tour starting with server index + +
      • +
      • + + Routing tour starting with client index + +
      • +
      +

      Pages router

      +
        +
      • + [static] useQueryState +
      • +
      • + + [dynamic] useQueryState + +
      • +
      • + [static] useQueryStates +
      • +
      • + + [dynamic] useQueryStates + +
      • +
      +
      + ) +} diff --git a/packages/playground/src/components/hydration-marker.tsx b/packages/e2e/src/components/hydration-marker.tsx similarity index 100% rename from packages/playground/src/components/hydration-marker.tsx rename to packages/e2e/src/components/hydration-marker.tsx diff --git a/packages/playground/src/pages/e2e/pages/useQueryState/dynamic/[route]/index.tsx b/packages/e2e/src/pages/pages/useQueryState/dynamic/[route]/index.tsx similarity index 100% rename from packages/playground/src/pages/e2e/pages/useQueryState/dynamic/[route]/index.tsx rename to packages/e2e/src/pages/pages/useQueryState/dynamic/[route]/index.tsx diff --git a/packages/playground/src/pages/e2e/pages/useQueryState/index.tsx b/packages/e2e/src/pages/pages/useQueryState/index.tsx similarity index 97% rename from packages/playground/src/pages/e2e/pages/useQueryState/index.tsx rename to packages/e2e/src/pages/pages/useQueryState/index.tsx index 25b36417..ffdba36d 100644 --- a/packages/playground/src/pages/e2e/pages/useQueryState/index.tsx +++ b/packages/e2e/src/pages/pages/useQueryState/index.tsx @@ -1,7 +1,7 @@ import { queryTypes, useQueryState } from 'next-usequerystate' import Link from 'next/link' import { usePathname } from 'next/navigation' -import { HydrationMarker } from '../../../../components/hydration-marker' +import { HydrationMarker } from '../../../components/hydration-marker' const IntegrationPage = () => { const [string, setString] = useQueryState('string') diff --git a/packages/playground/src/pages/e2e/pages/useQueryStates/dynamic/[route]/index.tsx b/packages/e2e/src/pages/pages/useQueryStates/dynamic/[route]/index.tsx similarity index 100% rename from packages/playground/src/pages/e2e/pages/useQueryStates/dynamic/[route]/index.tsx rename to packages/e2e/src/pages/pages/useQueryStates/dynamic/[route]/index.tsx diff --git a/packages/playground/src/pages/e2e/pages/useQueryStates/index.tsx b/packages/e2e/src/pages/pages/useQueryStates/index.tsx similarity index 94% rename from packages/playground/src/pages/e2e/pages/useQueryStates/index.tsx rename to packages/e2e/src/pages/pages/useQueryStates/index.tsx index b396de12..18a1acd4 100644 --- a/packages/playground/src/pages/e2e/pages/useQueryStates/index.tsx +++ b/packages/e2e/src/pages/pages/useQueryStates/index.tsx @@ -1,5 +1,5 @@ import { queryTypes, useQueryStates } from 'next-usequerystate' -import { HydrationMarker } from '../../../../components/hydration-marker' +import { HydrationMarker } from '../../../components/hydration-marker' const IntegrationPage = () => { const [state, setState] = useQueryStates({ diff --git a/packages/e2e/tsconfig.json b/packages/e2e/tsconfig.json new file mode 100644 index 00000000..5d83d444 --- /dev/null +++ b/packages/e2e/tsconfig.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + // Type checking + "strict": true, + "alwaysStrict": false, // Don't emit "use strict" to avoid conflicts with "use client" + // Modules + "module": "ESNext", + "moduleResolution": "node", + "resolveJsonModule": true, + // Language & Environment + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + // Emit + "noEmit": true, + "declaration": false, + "downlevelIteration": true, + "jsx": "preserve", + // Interop + "allowJs": true, + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + // Misc + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "incremental": true, + "tsBuildInfoFile": ".next/cache/.tsbuildinfo", + "plugins": [ + { + "name": "next" + } + ] + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/playground/package.json b/packages/playground/package.json index a24947ab..2c96d973 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -1,5 +1,5 @@ { - "name": "next-usequerystate-playground", + "name": "playground", "version": "0.0.0-internal", "description": "Examples and demos for next-usequerystate", "license": "MIT", @@ -13,11 +13,7 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start", - "pretest": "cypress install", - "test": "run-p --race start cypress:run", - "cypress:open": "cypress open", - "cypress:run": "cypress run --headless" + "start": "next start" }, "dependencies": { "next": "^14", @@ -30,8 +26,6 @@ "@types/react": "^18.2.33", "@types/react-dom": "^18.2.14", "@types/webpack": "^5.28.4", - "cypress": "^13.3.3", - "npm-run-all": "^4.1.5", "typescript": "^5.2.2" } } diff --git a/packages/playground/src/app/e2e/app/layout.tsx b/packages/playground/src/app/e2e/app/layout.tsx deleted file mode 100644 index 2e7597fa..00000000 --- a/packages/playground/src/app/e2e/app/layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Suspense } from 'react' -import { QuerySpy } from '../../../components/query-spy' - -export default function E2EPageLayout({ - children -}: { - children: React.ReactNode -}) { - return ( - <> - - - - {children} - - ) -} diff --git a/packages/playground/src/app/layout.tsx b/packages/playground/src/app/layout.tsx index 32d264b3..5b9ec493 100644 --- a/packages/playground/src/app/layout.tsx +++ b/packages/playground/src/app/layout.tsx @@ -1,6 +1,5 @@ import dynamic from 'next/dynamic' import React, { Suspense } from 'react' -import { HydrationMarker } from '../components/hydration-marker' export const metadata = { title: 'next-usequerystate playground', @@ -28,9 +27,6 @@ export default function RootLayout({ return ( - - -
      next-usequerystate diff --git a/packages/playground/src/app/page.tsx b/packages/playground/src/app/page.tsx index 3a38b557..2bdf4e9c 100644 --- a/packages/playground/src/app/page.tsx +++ b/packages/playground/src/app/page.tsx @@ -48,57 +48,6 @@ export default function IndexPage() { ))}

    -

    End-to-end integration tests

    -

    ⚠️ Don't change these routes without updating integration tests.

    -

    App router

    -
      -
    • - [static] useQueryState -
    • -
    • - - [dynamic] useQueryState - -
    • -
    • - [static] useQueryStates -
    • -
    • - - [dynamic] useQueryStates - -
    • -
    • - - Routing tour starting with server index - -
    • -
    • - - Routing tour starting with client index - -
    • -
    -

    Pages router

    -
      -
    • - [static] useQueryState -
    • -
    • - - [dynamic] useQueryState - -
    • -
    • - [static] useQueryStates -
    • -
    • - - [dynamic] useQueryStates - -
    • -
    -