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 usePathname to return string | null #42380

Merged
merged 2 commits into from Nov 3, 2022
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
10 changes: 2 additions & 8 deletions packages/next/client/components/navigation.ts
Expand Up @@ -76,7 +76,6 @@ export function useSearchParams() {
throw new Error('invariant expected search params to be mounted')
}

// eslint-disable-next-line react-hooks/rules-of-hooks
const readonlySearchParams = useMemo(() => {
return new ReadonlyURLSearchParams(searchParams)
}, [searchParams])
Expand All @@ -87,14 +86,9 @@ export function useSearchParams() {
/**
* Get the current pathname. For example usePathname() on /dashboard?foo=bar would return "/dashboard"
*/
export function usePathname(): string {
export function usePathname(): string | null {
staticGenerationBailout('usePathname')
const pathname = useContext(PathnameContext)
if (pathname === null) {
throw new Error('invariant expected pathname to be mounted')
}

return pathname
return useContext(PathnameContext)
}

// 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
14 changes: 7 additions & 7 deletions packages/next/client/index.tsx
Expand Up @@ -39,13 +39,10 @@ import { hasBasePath } from './has-base-path'
import { AppRouterContext } from '../shared/lib/app-router-context'
import {
adaptForAppRouterInstance,
adaptForPathname,
adaptForSearchParams,
PathnameContextProviderAdapter,
} from '../shared/lib/router/adapters'
import {
PathnameContext,
SearchParamsContext,
} from '../shared/lib/hooks-client-context'
import { SearchParamsContext } from '../shared/lib/hooks-client-context'

/// <reference types="react-dom/experimental" />

Expand Down Expand Up @@ -316,7 +313,10 @@ function AppContainer({
>
<AppRouterContext.Provider value={adaptForAppRouterInstance(router)}>
<SearchParamsContext.Provider value={adaptForSearchParams(router)}>
<PathnameContext.Provider value={adaptForPathname(asPath)}>
<PathnameContextProviderAdapter
router={router}
isAutoExport={self.__NEXT_DATA__.autoExport ?? false}
>
<RouterContext.Provider value={makePublicRouterInstance(router)}>
<HeadManagerContext.Provider value={headManager}>
<ImageConfigContext.Provider
Expand All @@ -328,7 +328,7 @@ function AppContainer({
</ImageConfigContext.Provider>
</HeadManagerContext.Provider>
</RouterContext.Provider>
</PathnameContext.Provider>
</PathnameContextProviderAdapter>
</SearchParamsContext.Provider>
</AppRouterContext.Provider>
</Container>
Expand Down
14 changes: 7 additions & 7 deletions packages/next/server/render.tsx
Expand Up @@ -84,14 +84,11 @@ import stripAnsi from 'next/dist/compiled/strip-ansi'
import { stripInternalQueries } from './internal-utils'
import {
adaptForAppRouterInstance,
adaptForPathname,
adaptForSearchParams,
PathnameContextProviderAdapter,
} from '../shared/lib/router/adapters'
import { AppRouterContext } from '../shared/lib/app-router-context'
import {
PathnameContext,
SearchParamsContext,
} from '../shared/lib/hooks-client-context'
import { SearchParamsContext } from '../shared/lib/hooks-client-context'

let tryGetPreviewData: typeof import('./api-utils/node').tryGetPreviewData
let warn: typeof import('../build/output/log').warn
Expand Down Expand Up @@ -621,7 +618,10 @@ export async function renderToHTML(
const AppContainer = ({ children }: { children: JSX.Element }) => (
<AppRouterContext.Provider value={appRouter}>
<SearchParamsContext.Provider value={adaptForSearchParams(router)}>
<PathnameContext.Provider value={adaptForPathname(asPath)}>
<PathnameContextProviderAdapter
router={router}
isAutoExport={isAutoExport}
>
<RouterContext.Provider value={router}>
<AmpStateContext.Provider value={ampState}>
<HeadManagerContext.Provider
Expand All @@ -648,7 +648,7 @@ export async function renderToHTML(
</HeadManagerContext.Provider>
</AmpStateContext.Provider>
</RouterContext.Provider>
</PathnameContext.Provider>
</PathnameContextProviderAdapter>
</SearchParamsContext.Provider>
</AppRouterContext.Provider>
)
Expand Down
81 changes: 0 additions & 81 deletions packages/next/shared/lib/router/adapters.ts

This file was deleted.

129 changes: 129 additions & 0 deletions packages/next/shared/lib/router/adapters.tsx
@@ -0,0 +1,129 @@
import type { ParsedUrlQuery } from 'node:querystring'
import React, { useMemo, useRef } from 'react'
import type { AppRouterInstance } from '../app-router-context'
import { PathnameContext } from '../hooks-client-context'
import type { NextRouter } from './router'
import { isDynamicRoute } from './utils'

/**
* adaptForAppRouterInstance implements the AppRouterInstance with a NextRouter.
*
* @param router the NextRouter to adapt
* @returns an AppRouterInstance
*/
export function adaptForAppRouterInstance(
router: NextRouter
): AppRouterInstance {
return {
back(): void {
router.back()
},
forward(): void {
router.forward()
},
refresh(): void {
router.reload()
},
push(href: string): void {
void router.push(href)
},
replace(href: string): void {
void router.replace(href)
},
prefetch(href: string): void {
void router.prefetch(href)
},
}
}

/**
* transforms the ParsedUrlQuery into a URLSearchParams.
*
* @param query the query to transform
* @returns URLSearchParams
*/
function transformQuery(query: ParsedUrlQuery): URLSearchParams {
const params = new URLSearchParams()

for (const [name, value] of Object.entries(query)) {
if (Array.isArray(value)) {
for (const val of value) {
params.append(name, val)
}
} else if (typeof value !== 'undefined') {
params.append(name, value)
}
}

return params
}

/**
* adaptForSearchParams transforms the ParsedURLQuery into URLSearchParams.
*
* @param router the router that contains the query.
* @returns the search params in the URLSearchParams format
*/
export function adaptForSearchParams(
router: Pick<NextRouter, 'isReady' | 'query'>
): URLSearchParams {
if (!router.isReady || !router.query) {
return new URLSearchParams()
}

return transformQuery(router.query)
}

export function PathnameContextProviderAdapter({
children,
router,
...props
}: React.PropsWithChildren<{
router: Pick<NextRouter, 'pathname' | 'asPath' | 'isReady' | 'isFallback'>
isAutoExport: boolean
}>) {
const ref = useRef(props.isAutoExport)
const value = useMemo(() => {
// isAutoExport is only ever `true` on the first render from the server,
// so reset it to `false` after we read it for the first time as `true`. If
// we don't use the value, then we don't need it.
const isAutoExport = ref.current
if (isAutoExport) {
ref.current = false
}

// When the route is a dynamic route, we need to do more processing to
// determine if we need to stop showing the pathname.
if (isDynamicRoute(router.pathname)) {
// When the router is rendering the fallback page, it can't possibly know
// the path, so return `null` here. Read more about fallback pages over
// at:
// https://nextjs.org/docs/api-reference/data-fetching/get-static-paths#fallback-pages
if (router.isFallback) {
return null
}

// When `isAutoExport` is true, meaning this is a page page has been
// automatically statically optimized, and the router is not ready, then
// we can't know the pathname yet. Read more about automatic static
// optimization at:
// https://nextjs.org/docs/advanced-features/automatic-static-optimization
if (isAutoExport && !router.isReady) {
return null
}
}

// The `router.asPath` contains the pathname seen by the browser (including
// any query strings), so it should have that stripped. Read more about the
// `asPath` option over at:
// https://nextjs.org/docs/api-reference/next/router#router-object
const url = new URL(router.asPath, 'http://f')
return url.pathname
}, [router.asPath, router.isFallback, router.isReady, router.pathname])

return (
<PathnameContext.Provider value={value}>
{children}
</PathnameContext.Provider>
)
}