From f1994a73b8d38f1b4573ef9d863714b8568078c3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 7 Nov 2022 15:11:58 -0800 Subject: [PATCH] feat: added new `next/compat/navigation` hooks --- packages/next/client/compat/navigation.ts | 53 ++++++++++++++++ packages/next/client/components/navigation.ts | 63 +++---------------- .../components/readonly-url-search-params.ts | 50 +++++++++++++++ packages/next/compat/navigation.d.ts | 1 + packages/next/compat/navigation.js | 1 + packages/next/shared/lib/router/adapters.tsx | 4 +- 6 files changed, 116 insertions(+), 56 deletions(-) create mode 100644 packages/next/client/compat/navigation.ts create mode 100644 packages/next/client/components/readonly-url-search-params.ts create mode 100644 packages/next/compat/navigation.d.ts create mode 100644 packages/next/compat/navigation.js diff --git a/packages/next/client/compat/navigation.ts b/packages/next/client/compat/navigation.ts new file mode 100644 index 00000000000000..075c0304d0b92f --- /dev/null +++ b/packages/next/client/compat/navigation.ts @@ -0,0 +1,53 @@ +import { useContext, useMemo } from 'react' +import { + PathnameContext, + SearchParamsContext, +} from '../../shared/lib/hooks-client-context' +import { ReadonlyURLSearchParams } from '../components/readonly-url-search-params' + +/** + * useRouter here is already fully backwards compatible in both `pages/` and in + * `app/`. + */ +export { useRouter } from '../components/navigation' + +/** + * usePathname from `next/compat/navigation`, much like the hook from + * `next/navigation` returns the pathname with the dynamic params substituted + * in. Unlike the hook in `next/navigation`, this will return `null` when + * the pathname is not available. + * + * This can only happen when the hook is used from a pages directory and the + * page being rendered has been automatically statically optimized or the page + * being rendered is the fallback page. + * + * @returns the pathname if available + */ +export function usePathname(): string | null { + return useContext(PathnameContext) +} + +/** + * useSearchParams from `next/compat/navigation`, much like the hook from + * `next/navigation` returns the URLSearchParams object for the search + * parameters. Unlike the hook in `next/navigation`, this will return `null` + * when the search params are not available. + * + * It will be `null` during prerendering if the page doesn't use Server-side + * Rendering. See https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props + * + * @returns the search params if available + */ +export function useSearchParams(): URLSearchParams | null { + const searchParams = useContext(SearchParamsContext) + + return useMemo(() => { + if (!searchParams) { + // When the router is not ready in pages, we won't have the search params + // available. + return null + } + + return new ReadonlyURLSearchParams(searchParams) + }, [searchParams]) +} diff --git a/packages/next/client/components/navigation.ts b/packages/next/client/components/navigation.ts index 66003cafeaafab..e5de7726ac0e1d 100644 --- a/packages/next/client/components/navigation.ts +++ b/packages/next/client/components/navigation.ts @@ -12,59 +12,9 @@ import { PathnameContext, // LayoutSegmentsContext, } from '../../shared/lib/hooks-client-context' +import { ReadonlyURLSearchParams } from './readonly-url-search-params' import { staticGenerationBailout } from './static-generation-bailout' -const INTERNAL_URLSEARCHPARAMS_INSTANCE = Symbol( - 'internal for urlsearchparams readonly' -) - -function readonlyURLSearchParamsError() { - return new Error('ReadonlyURLSearchParams cannot be modified') -} - -class ReadonlyURLSearchParams { - [INTERNAL_URLSEARCHPARAMS_INSTANCE]: URLSearchParams - - entries: URLSearchParams['entries'] - forEach: URLSearchParams['forEach'] - get: URLSearchParams['get'] - getAll: URLSearchParams['getAll'] - has: URLSearchParams['has'] - keys: URLSearchParams['keys'] - values: URLSearchParams['values'] - toString: URLSearchParams['toString'] - - constructor(urlSearchParams: URLSearchParams) { - // Since `new Headers` uses `this.append()` to fill the headers object ReadonlyHeaders can't extend from Headers directly as it would throw. - this[INTERNAL_URLSEARCHPARAMS_INSTANCE] = urlSearchParams - - this.entries = urlSearchParams.entries.bind(urlSearchParams) - this.forEach = urlSearchParams.forEach.bind(urlSearchParams) - this.get = urlSearchParams.get.bind(urlSearchParams) - this.getAll = urlSearchParams.getAll.bind(urlSearchParams) - this.has = urlSearchParams.has.bind(urlSearchParams) - this.keys = urlSearchParams.keys.bind(urlSearchParams) - this.values = urlSearchParams.values.bind(urlSearchParams) - this.toString = urlSearchParams.toString.bind(urlSearchParams) - } - [Symbol.iterator]() { - return this[INTERNAL_URLSEARCHPARAMS_INSTANCE][Symbol.iterator]() - } - - append() { - throw readonlyURLSearchParamsError() - } - delete() { - throw readonlyURLSearchParamsError() - } - set() { - throw readonlyURLSearchParamsError() - } - sort() { - throw readonlyURLSearchParamsError() - } -} - /** * Get a read-only URLSearchParams object. For example searchParams.get('foo') would return 'bar' when ?foo=bar * Learn more about URLSearchParams here: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams @@ -73,7 +23,7 @@ export function useSearchParams() { staticGenerationBailout('useSearchParams') const searchParams = useContext(SearchParamsContext) if (!searchParams) { - throw new Error('invariant expected search params to be mounted') + throw new Error('invariant expected search params context to be mounted') } const readonlySearchParams = useMemo(() => { @@ -86,8 +36,13 @@ export function useSearchParams() { /** * Get the current pathname. For example usePathname() on /dashboard?foo=bar would return "/dashboard" */ -export function usePathname(): string | null { - return useContext(PathnameContext) +export function usePathname(): string { + const pathname = useContext(PathnameContext) + if (pathname === null) { + throw new Error('invariant expected pathname context to be mounted') + } + + return pathname } // TODO-APP: getting all params when client-side navigating is non-trivial as it does not have route matchers so this might have to be a server context instead. diff --git a/packages/next/client/components/readonly-url-search-params.ts b/packages/next/client/components/readonly-url-search-params.ts new file mode 100644 index 00000000000000..d88e7a36e37339 --- /dev/null +++ b/packages/next/client/components/readonly-url-search-params.ts @@ -0,0 +1,50 @@ +const INTERNAL_URLSEARCHPARAMS_INSTANCE = Symbol( + 'internal for urlsearchparams readonly' +) + +function readonlyURLSearchParamsError() { + return new Error('ReadonlyURLSearchParams cannot be modified') +} + +export class ReadonlyURLSearchParams { + [INTERNAL_URLSEARCHPARAMS_INSTANCE]: URLSearchParams + + entries: URLSearchParams['entries'] + forEach: URLSearchParams['forEach'] + get: URLSearchParams['get'] + getAll: URLSearchParams['getAll'] + has: URLSearchParams['has'] + keys: URLSearchParams['keys'] + values: URLSearchParams['values'] + toString: URLSearchParams['toString'] + + constructor(urlSearchParams: URLSearchParams) { + // Since `new Headers` uses `this.append()` to fill the headers object ReadonlyHeaders can't extend from Headers directly as it would throw. + this[INTERNAL_URLSEARCHPARAMS_INSTANCE] = urlSearchParams + + this.entries = urlSearchParams.entries.bind(urlSearchParams) + this.forEach = urlSearchParams.forEach.bind(urlSearchParams) + this.get = urlSearchParams.get.bind(urlSearchParams) + this.getAll = urlSearchParams.getAll.bind(urlSearchParams) + this.has = urlSearchParams.has.bind(urlSearchParams) + this.keys = urlSearchParams.keys.bind(urlSearchParams) + this.values = urlSearchParams.values.bind(urlSearchParams) + this.toString = urlSearchParams.toString.bind(urlSearchParams) + } + [Symbol.iterator]() { + return this[INTERNAL_URLSEARCHPARAMS_INSTANCE][Symbol.iterator]() + } + + append() { + throw readonlyURLSearchParamsError() + } + delete() { + throw readonlyURLSearchParamsError() + } + set() { + throw readonlyURLSearchParamsError() + } + sort() { + throw readonlyURLSearchParamsError() + } +} diff --git a/packages/next/compat/navigation.d.ts b/packages/next/compat/navigation.d.ts new file mode 100644 index 00000000000000..425388738f5d2e --- /dev/null +++ b/packages/next/compat/navigation.d.ts @@ -0,0 +1 @@ +export * from '../dist/client/compat/navigation' diff --git a/packages/next/compat/navigation.js b/packages/next/compat/navigation.js new file mode 100644 index 00000000000000..2b8b808779dbd0 --- /dev/null +++ b/packages/next/compat/navigation.js @@ -0,0 +1 @@ +module.exports = require('../dist/client/compat/navigation') diff --git a/packages/next/shared/lib/router/adapters.tsx b/packages/next/shared/lib/router/adapters.tsx index 9bf0096abad2be..6c37cc35ab2f91 100644 --- a/packages/next/shared/lib/router/adapters.tsx +++ b/packages/next/shared/lib/router/adapters.tsx @@ -66,9 +66,9 @@ function transformQuery(query: ParsedUrlQuery): URLSearchParams { */ export function adaptForSearchParams( router: Pick -): URLSearchParams { +): URLSearchParams | null { if (!router.isReady || !router.query) { - return new URLSearchParams() + return null } return transformQuery(router.query)