Skip to content

Commit

Permalink
feat: added new next/compat/navigation hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
wyattjoh committed Nov 7, 2022
1 parent 7bbc1ae commit f1994a7
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 56 deletions.
53 changes: 53 additions & 0 deletions packages/next/client/compat/navigation.ts
Original file line number Diff line number Diff line change
@@ -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])
}
63 changes: 9 additions & 54 deletions packages/next/client/components/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(() => {
Expand All @@ -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.
Expand Down
50 changes: 50 additions & 0 deletions packages/next/client/components/readonly-url-search-params.ts
Original file line number Diff line number Diff line change
@@ -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()
}
}
1 change: 1 addition & 0 deletions packages/next/compat/navigation.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../dist/client/compat/navigation'
1 change: 1 addition & 0 deletions packages/next/compat/navigation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../dist/client/compat/navigation')
4 changes: 2 additions & 2 deletions packages/next/shared/lib/router/adapters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ function transformQuery(query: ParsedUrlQuery): URLSearchParams {
*/
export function adaptForSearchParams(
router: Pick<NextRouter, 'isReady' | 'query'>
): URLSearchParams {
): URLSearchParams | null {
if (!router.isReady || !router.query) {
return new URLSearchParams()
return null
}

return transformQuery(router.query)
Expand Down

0 comments on commit f1994a7

Please sign in to comment.