From 482e3fbffa282131f4b718877810a640a08b10e1 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Sat, 15 Oct 2022 21:16:50 +0200 Subject: [PATCH] Change useSearchParams to URLSearchParams (#40978) Similar to #40872 `useSearchParams` now returns a `URLSearchParams` instance instead of a plain object. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- .../next/client/components/app-router.tsx | 11 ++- .../client/components/hooks-client-context.ts | 5 +- packages/next/client/components/navigation.ts | 67 ++++++++++++++++--- .../app/app/hooks/use-search-params/page.js | 8 +-- 4 files changed, 67 insertions(+), 24 deletions(-) diff --git a/packages/next/client/components/app-router.tsx b/packages/next/client/components/app-router.tsx index fca7b25138b86..073682caff8f6 100644 --- a/packages/next/client/components/app-router.tsx +++ b/packages/next/client/components/app-router.tsx @@ -147,12 +147,11 @@ function Router({ typeof window === 'undefined' ? 'http://n' : window.location.href ) - // Convert searchParams to a plain object to match server-side. - const searchParamsObj: { [key: string]: string } = {} - url.searchParams.forEach((value, key) => { - searchParamsObj[key] = value - }) - return { searchParams: searchParamsObj, pathname: url.pathname } + return { + // This is turned into a readonly class in `useSearchParams` + searchParams: url.searchParams, + pathname: url.pathname, + } }, [canonicalUrl]) /** diff --git a/packages/next/client/components/hooks-client-context.ts b/packages/next/client/components/hooks-client-context.ts index 98251b90d6fae..3fdc5395a7cfc 100644 --- a/packages/next/client/components/hooks-client-context.ts +++ b/packages/next/client/components/hooks-client-context.ts @@ -1,9 +1,6 @@ import { createContext } from 'react' -import type { NextParsedUrlQuery } from '../../server/request-meta' -export const SearchParamsContext = createContext( - null as any -) +export const SearchParamsContext = createContext(null as any) export const PathnameContext = createContext(null as any) export const ParamsContext = createContext(null as any) export const LayoutSegmentsContext = createContext(null as any) diff --git a/packages/next/client/components/navigation.ts b/packages/next/client/components/navigation.ts index eb7a06f39ec05..c4f45db13ed06 100644 --- a/packages/next/client/components/navigation.ts +++ b/packages/next/client/components/navigation.ts @@ -1,6 +1,6 @@ // useLayoutSegments() // Only the segments for the current place. ['children', 'dashboard', 'children', 'integrations'] -> /dashboard/integrations (/dashboard/layout.js would get ['children', 'dashboard', 'children', 'integrations']) -import { useContext } from 'react' +import { useContext, useMemo } from 'react' import { SearchParamsContext, // ParamsContext, @@ -17,19 +17,66 @@ export { useServerInsertedHTML, } from '../../shared/lib/server-inserted-html' -/** - * Get the current search params. For example useSearchParams() would return {"foo": "bar"} when ?foo=bar - */ -export function useSearchParams() { - return useContext(SearchParamsContext) +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 an individual search param. For example useSearchParam("foo") would return "bar" when ?foo=bar + * Get the current search params. For example useSearchParams() would return {"foo": "bar"} when ?foo=bar */ -export function useSearchParam(key: string): string | string[] { - const params = useContext(SearchParamsContext) - return params[key] +export function useSearchParams() { + const searchParams = useContext(SearchParamsContext) + const readonlySearchParams = useMemo(() => { + return new ReadonlyURLSearchParams(searchParams) + }, [searchParams]) + return readonlySearchParams } // TODO-APP: Move the other router context over to this one diff --git a/test/e2e/app-dir/app/app/hooks/use-search-params/page.js b/test/e2e/app-dir/app/app/hooks/use-search-params/page.js index 1f6a419df4fb6..4fb6bb13a375e 100644 --- a/test/e2e/app-dir/app/app/hooks/use-search-params/page.js +++ b/test/e2e/app-dir/app/app/hooks/use-search-params/page.js @@ -9,10 +9,10 @@ export default function Page() { <>

hello from /hooks/use-search-params