Skip to content

Commit 1f55ba3

Browse files
authored
Change usePathname to return string | null (#42380)
This changes the API of `usePathname` to return `string | null` to support hybrid use-cases where the pathname is unknown at build time (during automatic static optimization and when fallback is set true with dynamic parameters in the pathname). This supports a cleaner DX experience for those moving from `pages/` to `app/` so they can begin to use `usePathname` in components that are shared across them.
1 parent e74de1a commit 1f55ba3

File tree

5 files changed

+145
-103
lines changed

5 files changed

+145
-103
lines changed

packages/next/client/components/navigation.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ export function useSearchParams() {
7676
throw new Error('invariant expected search params to be mounted')
7777
}
7878

79-
// eslint-disable-next-line react-hooks/rules-of-hooks
8079
const readonlySearchParams = useMemo(() => {
8180
return new ReadonlyURLSearchParams(searchParams)
8281
}, [searchParams])
@@ -87,14 +86,9 @@ export function useSearchParams() {
8786
/**
8887
* Get the current pathname. For example usePathname() on /dashboard?foo=bar would return "/dashboard"
8988
*/
90-
export function usePathname(): string {
89+
export function usePathname(): string | null {
9190
staticGenerationBailout('usePathname')
92-
const pathname = useContext(PathnameContext)
93-
if (pathname === null) {
94-
throw new Error('invariant expected pathname to be mounted')
95-
}
96-
97-
return pathname
91+
return useContext(PathnameContext)
9892
}
9993

10094
// 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.

packages/next/client/index.tsx

+7-7
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,10 @@ import { hasBasePath } from './has-base-path'
3939
import { AppRouterContext } from '../shared/lib/app-router-context'
4040
import {
4141
adaptForAppRouterInstance,
42-
adaptForPathname,
4342
adaptForSearchParams,
43+
PathnameContextProviderAdapter,
4444
} from '../shared/lib/router/adapters'
45-
import {
46-
PathnameContext,
47-
SearchParamsContext,
48-
} from '../shared/lib/hooks-client-context'
45+
import { SearchParamsContext } from '../shared/lib/hooks-client-context'
4946

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

@@ -316,7 +313,10 @@ function AppContainer({
316313
>
317314
<AppRouterContext.Provider value={adaptForAppRouterInstance(router)}>
318315
<SearchParamsContext.Provider value={adaptForSearchParams(router)}>
319-
<PathnameContext.Provider value={adaptForPathname(asPath)}>
316+
<PathnameContextProviderAdapter
317+
router={router}
318+
isAutoExport={self.__NEXT_DATA__.autoExport ?? false}
319+
>
320320
<RouterContext.Provider value={makePublicRouterInstance(router)}>
321321
<HeadManagerContext.Provider value={headManager}>
322322
<ImageConfigContext.Provider
@@ -328,7 +328,7 @@ function AppContainer({
328328
</ImageConfigContext.Provider>
329329
</HeadManagerContext.Provider>
330330
</RouterContext.Provider>
331-
</PathnameContext.Provider>
331+
</PathnameContextProviderAdapter>
332332
</SearchParamsContext.Provider>
333333
</AppRouterContext.Provider>
334334
</Container>

packages/next/server/render.tsx

+7-7
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,11 @@ import stripAnsi from 'next/dist/compiled/strip-ansi'
8484
import { stripInternalQueries } from './internal-utils'
8585
import {
8686
adaptForAppRouterInstance,
87-
adaptForPathname,
8887
adaptForSearchParams,
88+
PathnameContextProviderAdapter,
8989
} from '../shared/lib/router/adapters'
9090
import { AppRouterContext } from '../shared/lib/app-router-context'
91-
import {
92-
PathnameContext,
93-
SearchParamsContext,
94-
} from '../shared/lib/hooks-client-context'
91+
import { SearchParamsContext } from '../shared/lib/hooks-client-context'
9592

9693
let tryGetPreviewData: typeof import('./api-utils/node').tryGetPreviewData
9794
let warn: typeof import('../build/output/log').warn
@@ -621,7 +618,10 @@ export async function renderToHTML(
621618
const AppContainer = ({ children }: { children: JSX.Element }) => (
622619
<AppRouterContext.Provider value={appRouter}>
623620
<SearchParamsContext.Provider value={adaptForSearchParams(router)}>
624-
<PathnameContext.Provider value={adaptForPathname(asPath)}>
621+
<PathnameContextProviderAdapter
622+
router={router}
623+
isAutoExport={isAutoExport}
624+
>
625625
<RouterContext.Provider value={router}>
626626
<AmpStateContext.Provider value={ampState}>
627627
<HeadManagerContext.Provider
@@ -648,7 +648,7 @@ export async function renderToHTML(
648648
</HeadManagerContext.Provider>
649649
</AmpStateContext.Provider>
650650
</RouterContext.Provider>
651-
</PathnameContext.Provider>
651+
</PathnameContextProviderAdapter>
652652
</SearchParamsContext.Provider>
653653
</AppRouterContext.Provider>
654654
)

packages/next/shared/lib/router/adapters.ts

-81
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import type { ParsedUrlQuery } from 'node:querystring'
2+
import React, { useMemo, useRef } from 'react'
3+
import type { AppRouterInstance } from '../app-router-context'
4+
import { PathnameContext } from '../hooks-client-context'
5+
import type { NextRouter } from './router'
6+
import { isDynamicRoute } from './utils'
7+
8+
/**
9+
* adaptForAppRouterInstance implements the AppRouterInstance with a NextRouter.
10+
*
11+
* @param router the NextRouter to adapt
12+
* @returns an AppRouterInstance
13+
*/
14+
export function adaptForAppRouterInstance(
15+
router: NextRouter
16+
): AppRouterInstance {
17+
return {
18+
back(): void {
19+
router.back()
20+
},
21+
forward(): void {
22+
router.forward()
23+
},
24+
refresh(): void {
25+
router.reload()
26+
},
27+
push(href: string): void {
28+
void router.push(href)
29+
},
30+
replace(href: string): void {
31+
void router.replace(href)
32+
},
33+
prefetch(href: string): void {
34+
void router.prefetch(href)
35+
},
36+
}
37+
}
38+
39+
/**
40+
* transforms the ParsedUrlQuery into a URLSearchParams.
41+
*
42+
* @param query the query to transform
43+
* @returns URLSearchParams
44+
*/
45+
function transformQuery(query: ParsedUrlQuery): URLSearchParams {
46+
const params = new URLSearchParams()
47+
48+
for (const [name, value] of Object.entries(query)) {
49+
if (Array.isArray(value)) {
50+
for (const val of value) {
51+
params.append(name, val)
52+
}
53+
} else if (typeof value !== 'undefined') {
54+
params.append(name, value)
55+
}
56+
}
57+
58+
return params
59+
}
60+
61+
/**
62+
* adaptForSearchParams transforms the ParsedURLQuery into URLSearchParams.
63+
*
64+
* @param router the router that contains the query.
65+
* @returns the search params in the URLSearchParams format
66+
*/
67+
export function adaptForSearchParams(
68+
router: Pick<NextRouter, 'isReady' | 'query'>
69+
): URLSearchParams {
70+
if (!router.isReady || !router.query) {
71+
return new URLSearchParams()
72+
}
73+
74+
return transformQuery(router.query)
75+
}
76+
77+
export function PathnameContextProviderAdapter({
78+
children,
79+
router,
80+
...props
81+
}: React.PropsWithChildren<{
82+
router: Pick<NextRouter, 'pathname' | 'asPath' | 'isReady' | 'isFallback'>
83+
isAutoExport: boolean
84+
}>) {
85+
const ref = useRef(props.isAutoExport)
86+
const value = useMemo(() => {
87+
// isAutoExport is only ever `true` on the first render from the server,
88+
// so reset it to `false` after we read it for the first time as `true`. If
89+
// we don't use the value, then we don't need it.
90+
const isAutoExport = ref.current
91+
if (isAutoExport) {
92+
ref.current = false
93+
}
94+
95+
// When the route is a dynamic route, we need to do more processing to
96+
// determine if we need to stop showing the pathname.
97+
if (isDynamicRoute(router.pathname)) {
98+
// When the router is rendering the fallback page, it can't possibly know
99+
// the path, so return `null` here. Read more about fallback pages over
100+
// at:
101+
// https://nextjs.org/docs/api-reference/data-fetching/get-static-paths#fallback-pages
102+
if (router.isFallback) {
103+
return null
104+
}
105+
106+
// When `isAutoExport` is true, meaning this is a page page has been
107+
// automatically statically optimized, and the router is not ready, then
108+
// we can't know the pathname yet. Read more about automatic static
109+
// optimization at:
110+
// https://nextjs.org/docs/advanced-features/automatic-static-optimization
111+
if (isAutoExport && !router.isReady) {
112+
return null
113+
}
114+
}
115+
116+
// The `router.asPath` contains the pathname seen by the browser (including
117+
// any query strings), so it should have that stripped. Read more about the
118+
// `asPath` option over at:
119+
// https://nextjs.org/docs/api-reference/next/router#router-object
120+
const url = new URL(router.asPath, 'http://f')
121+
return url.pathname
122+
}, [router.asPath, router.isFallback, router.isReady, router.pathname])
123+
124+
return (
125+
<PathnameContext.Provider value={value}>
126+
{children}
127+
</PathnameContext.Provider>
128+
)
129+
}

0 commit comments

Comments
 (0)