Skip to content

Commit

Permalink
Change useSearchParams to URLSearchParams (#40978)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
timneutkens committed Oct 15, 2022
1 parent ad1e8dd commit 482e3fb
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 24 deletions.
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']

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

0 comments on commit 482e3fb

Please sign in to comment.