Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change useSearchParams to URLSearchParams #40978

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 5 additions & 6 deletions packages/next/client/components/app-router.tsx
Expand Up @@ -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])

/**
Expand Down
5 changes: 1 addition & 4 deletions 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<NextParsedUrlQuery>(
null as any
)
export const SearchParamsContext = createContext<URLSearchParams>(null as any)
export const PathnameContext = createContext<string>(null as any)
export const ParamsContext = createContext(null as any)
export const LayoutSegmentsContext = createContext(null as any)
Expand Down
67 changes: 57 additions & 10 deletions 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,
Expand All @@ -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']

huozhi marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
8 changes: 4 additions & 4 deletions test/e2e/app-dir/app/app/hooks/use-search-params/page.js
Expand Up @@ -9,10 +9,10 @@ export default function Page() {
<>
<h1
id="params"
data-param-first={params.first ?? 'N/A'}
data-param-second={params.second ?? 'N/A'}
data-param-third={params.third ?? 'N/A'}
data-param-not-real={params.notReal ?? 'N/A'}
data-param-first={params.get('first') ?? 'N/A'}
data-param-second={params.get('second') ?? 'N/A'}
data-param-third={params.get('third') ?? 'N/A'}
data-param-not-real={params.get('notReal') ?? 'N/A'}
>
hello from /hooks/use-search-params
</h1>
Expand Down