Skip to content

Commit

Permalink
Hybrid App Hooks Support (#41767)
Browse files Browse the repository at this point in the history
This adapts the new client hooks of `usePathname`, `useSearchParams`,
and `useRouter` to work within the `pages/` directory to aid users
attempting to migrate shared components over to the `app/` directory.

> **Exception:**
> When the pages router is not ready, `useSearchParams` will return an
empty `URLSearchParams`. This mirrors the behavior seen in the `pages/`
directory today in that `router.query` is not available until the client
hydrates.

This also adds a new option for `useRouter` to bring it line with the
correct typings with the app directory. By default, calling
`useRouter()` will return the type `NextRouter | null` to represent what
you get when you call it from a component originating from the app
directory. If you want to instead force it to return `NextRouter` as it
does today, you can pass a boolean into the `useRouter` call as such:

```ts
const router = useRouter()     // typeof router === NextRouter | null
const router = useRouter(true) // typeof router === NextRouter
```

This change is designed to ease the incremental adoption of app.
  • Loading branch information
wyattjoh committed Nov 1, 2022
1 parent b0ad8ad commit 6c7e76b
Show file tree
Hide file tree
Showing 23 changed files with 493 additions and 136 deletions.
7 changes: 5 additions & 2 deletions .eslintrc.json
Expand Up @@ -238,7 +238,6 @@
"no-obj-calls": "warn",
"no-octal": "warn",
"no-octal-escape": "warn",
"no-redeclare": ["warn", { "builtinGlobals": false }],
"no-regex-spaces": "warn",
"no-restricted-syntax": [
"warn",
Expand Down Expand Up @@ -330,6 +329,10 @@
"react/style-prop-object": "warn",
"react-hooks/rules-of-hooks": "error",
// "@typescript-eslint/non-nullable-type-assertion-style": "warn",
"@typescript-eslint/prefer-as-const": "warn"
"@typescript-eslint/prefer-as-const": "warn",
"@typescript-eslint/no-redeclare": [
"warn",
{ "builtinGlobals": false, "ignoreDeclarationMerge": true }
]
}
}
2 changes: 1 addition & 1 deletion packages/next/client/components/app-router.tsx
Expand Up @@ -26,7 +26,7 @@ import {
// ParamsContext,
PathnameContext,
// LayoutSegmentsContext,
} from './hooks-client-context'
} from '../../shared/lib/hooks-client-context'
import { useReducerWithReduxDevtools } from './use-reducer-with-devtools'
import { ErrorBoundary, GlobalErrorComponent } from './error-boundary'

Expand Down
25 changes: 16 additions & 9 deletions packages/next/client/components/layout-router.tsx
Expand Up @@ -19,12 +19,12 @@ import {
LayoutRouterContext,
GlobalLayoutRouterContext,
TemplateContext,
AppRouterContext,
} from '../../shared/lib/app-router-context'
import { fetchServerResponse } from './app-router'
import { createInfinitePromise } from './infinite-promise'
import { ErrorBoundary } from './error-boundary'
import { matchSegment } from './match-segments'
import { useRouter } from './navigation'

/**
* Add refetch marker to router state at the point of the current layout segment.
Expand Down Expand Up @@ -109,11 +109,13 @@ export function InnerLayoutRouter({
path: string
rootLayoutIncluded: boolean
}) {
const {
changeByServerResponse,
tree: fullTree,
focusAndScrollRef,
} = useContext(GlobalLayoutRouterContext)
const context = useContext(GlobalLayoutRouterContext)
if (!context) {
throw new Error('invariant global layout router not mounted')
}

const { changeByServerResponse, tree: fullTree, focusAndScrollRef } = context

const focusAndScrollElementRef = useRef<HTMLDivElement>(null)

useEffect(() => {
Expand Down Expand Up @@ -275,7 +277,7 @@ interface RedirectBoundaryProps {
}

function HandleRedirect({ redirect }: { redirect: string }) {
const router = useContext(AppRouterContext)
const router = useRouter()

useEffect(() => {
router.replace(redirect, {})
Expand Down Expand Up @@ -312,7 +314,7 @@ class RedirectErrorBoundary extends React.Component<
}

function RedirectBoundary({ children }: { children: React.ReactNode }) {
const router = useContext(AppRouterContext)
const router = useRouter()
return (
<RedirectErrorBoundary router={router}>{children}</RedirectErrorBoundary>
)
Expand Down Expand Up @@ -389,7 +391,12 @@ export default function OuterLayoutRouter({
notFound: React.ReactNode | undefined
rootLayoutIncluded: boolean
}) {
const { childNodes, tree, url } = useContext(LayoutRouterContext)
const context = useContext(LayoutRouterContext)
if (!context) {
throw new Error('invariant expected layout router to be mounted')
}

const { childNodes, tree, url } = context

// Get the current parallelRouter cache node
let childNodesForParallelRouter = childNodes.get(parallelRouterKey)
Expand Down
22 changes: 19 additions & 3 deletions packages/next/client/components/navigation.ts
Expand Up @@ -11,7 +11,7 @@ import {
// ParamsContext,
PathnameContext,
// LayoutSegmentsContext,
} from './hooks-client-context'
} from '../../shared/lib/hooks-client-context'
import { staticGenerationBailout } from './static-generation-bailout'

const INTERNAL_URLSEARCHPARAMS_INSTANCE = Symbol(
Expand Down Expand Up @@ -72,9 +72,15 @@ class ReadonlyURLSearchParams {
export function useSearchParams() {
staticGenerationBailout('useSearchParams')
const searchParams = useContext(SearchParamsContext)
if (!searchParams) {
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])

return readonlySearchParams
}

Expand All @@ -83,7 +89,12 @@ export function useSearchParams() {
*/
export function usePathname(): string {
staticGenerationBailout('usePathname')
return useContext(PathnameContext)
const pathname = useContext(PathnameContext)
if (pathname === null) {
throw new Error('invariant expected pathname 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 All @@ -106,7 +117,12 @@ export {
* Get the router methods. For example router.push('/dashboard')
*/
export function useRouter(): import('../../shared/lib/app-router-context').AppRouterInstance {
return useContext(AppRouterContext)
const router = useContext(AppRouterContext)
if (router === null) {
throw new Error('invariant expected app router to be mounted')
}

return router
}

// TODO-APP: handle parallel routes
Expand Down
36 changes: 27 additions & 9 deletions packages/next/client/index.tsx
Expand Up @@ -36,6 +36,16 @@ import { ImageConfigContext } from '../shared/lib/image-config-context'
import { ImageConfigComplete } from '../shared/lib/image-config'
import { removeBasePath } from './remove-base-path'
import { hasBasePath } from './has-base-path'
import { AppRouterContext } from '../shared/lib/app-router-context'
import {
adaptForAppRouterInstance,
adaptForPathname,
adaptForSearchParams,
} from '../shared/lib/router/adapters'
import {
PathnameContext,
SearchParamsContext,
} from '../shared/lib/hooks-client-context'

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

Expand Down Expand Up @@ -304,15 +314,23 @@ function AppContainer({
)
}
>
<RouterContext.Provider value={makePublicRouterInstance(router)}>
<HeadManagerContext.Provider value={headManager}>
<ImageConfigContext.Provider
value={process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete}
>
{children}
</ImageConfigContext.Provider>
</HeadManagerContext.Provider>
</RouterContext.Provider>
<AppRouterContext.Provider value={adaptForAppRouterInstance(router)}>
<SearchParamsContext.Provider value={adaptForSearchParams(router)}>
<PathnameContext.Provider value={adaptForPathname(asPath)}>
<RouterContext.Provider value={makePublicRouterInstance(router)}>
<HeadManagerContext.Provider value={headManager}>
<ImageConfigContext.Provider
value={
process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
}
>
{children}
</ImageConfigContext.Provider>
</HeadManagerContext.Provider>
</RouterContext.Provider>
</PathnameContext.Provider>
</SearchParamsContext.Provider>
</AppRouterContext.Provider>
</Container>
)
}
Expand Down

0 comments on commit 6c7e76b

Please sign in to comment.