Skip to content

Commit

Permalink
feat: initial hybrid hook support
Browse files Browse the repository at this point in the history
  • Loading branch information
wyattjoh committed Oct 26, 2022
1 parent fb8febd commit a100813
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 74 deletions.
24 changes: 16 additions & 8 deletions packages/next/client/components/layout-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ 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 +110,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 @@ -273,7 +276,7 @@ interface RedirectBoundaryProps {
}

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

useEffect(() => {
router.replace(redirect, {})
Expand Down Expand Up @@ -310,7 +313,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 @@ -387,7 +390,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
20 changes: 18 additions & 2 deletions packages/next/client/components/navigation.ts
Original file line number Diff line number Diff line change
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
24 changes: 15 additions & 9 deletions packages/next/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ 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 } from '../shared/lib/router/adapters'

const ReactDOM = process.env.__NEXT_REACT_ROOT
? require('react-dom/client')
Expand Down Expand Up @@ -306,15 +308,19 @@ 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)}>
<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>
</Container>
)
}
Expand Down
81 changes: 55 additions & 26 deletions packages/next/client/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
PrefetchOptions,
resolveHref,
} from '../shared/lib/router/router'
import { formatUrl } from '../shared/lib/router/utils/format-url'
import { addLocale } from './add-locale'
import { RouterContext } from '../shared/lib/router-context'
import {
Expand All @@ -17,6 +18,7 @@ import {
import { useIntersection } from './use-intersection'
import { getDomainLocale } from './get-domain-locale'
import { addBasePath } from './add-base-path'
import { useRouter } from './components/navigation'

type Url = string | UrlObject
type RequiredKeys<T> = {
Expand Down Expand Up @@ -108,12 +110,13 @@ type LinkPropsOptional = OptionalKeys<InternalLinkProps>
const prefetched: { [cacheKey: string]: boolean } = {}

function prefetch(
router: NextRouter,
router: NextRouter | AppRouterInstance,
href: string,
as: string,
options?: PrefetchOptions
): void {
if (typeof window === 'undefined' || !router) return

if (!isLocalURL(href)) return
// Prefetch the JSON page if asked (only in the client)
// We need to handle a prefetch error here since we may be
Expand All @@ -125,10 +128,13 @@ function prefetch(
throw err
}
})

const curLocale =
options && typeof options.locale !== 'undefined'
? options.locale
: router && router.locale
: 'locale' in router
? router.locale
: undefined

// Join on an invalid URI character
prefetched[href + '%' + as + (curLocale ? '%' + curLocale : '')] = true
Expand Down Expand Up @@ -179,11 +185,7 @@ function linkClicked(
scroll,
})
} else {
// If `beforePopState` doesn't exist on the router it's the AppRouter.
const method: keyof AppRouterInstance = replace ? 'replace' : 'push'

// Apply `as` if it's provided.
router[method](as || href, {
router[replace ? 'replace' : 'push'](as || href, {
forceOptimisticNavigation: !prefetchEnabled,
})
}
Expand All @@ -202,6 +204,14 @@ type LinkPropsReal = React.PropsWithChildren<
LinkProps
>

function formatStringOrUrl(urlObjOrString: UrlObject | string): string {
if (typeof urlObjOrString === 'string') {
return urlObjOrString
}

return formatUrl(urlObjOrString)
}

/**
* React Component that enables client-side transitions between routes.
*/
Expand Down Expand Up @@ -357,21 +367,36 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
}

const p = prefetchProp !== false
let router = React.useContext(RouterContext)

// TODO-APP: type error. Remove `as any`
const appRouter = React.useContext(AppRouterContext) as any
if (appRouter) {
router = appRouter
}
const pagesRouter = React.useContext(RouterContext)
const appRouter = useRouter()
const router = pagesRouter ?? appRouter

// We're in the app directory if there is no pages router.
const isAppRouter = !pagesRouter

const { href, as } = React.useMemo(() => {
const [resolvedHref, resolvedAs] = resolveHref(router, hrefProp, true)
if (!pagesRouter) {
const resolvedHref = formatStringOrUrl(hrefProp)
return {
href: resolvedHref,
as: asProp ? formatStringOrUrl(asProp) : resolvedHref,
}
}

const [resolvedHref, resolvedAs] = resolveHref(
pagesRouter,
hrefProp,
true
)

return {
href: resolvedHref,
as: asProp ? resolveHref(router, asProp) : resolvedAs || resolvedHref,
as: asProp
? resolveHref(pagesRouter, asProp)
: resolvedAs || resolvedHref,
}
}, [router, hrefProp, asProp])
}, [pagesRouter, hrefProp, asProp])

const previousHref = React.useRef<string>(href)
const previousAs = React.useRef<string>(as)
Expand Down Expand Up @@ -448,15 +473,15 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
React.useEffect(() => {
const shouldPrefetch = isVisible && p && isLocalURL(href)
const curLocale =
typeof locale !== 'undefined' ? locale : router && router.locale
typeof locale !== 'undefined' ? locale : pagesRouter?.locale
const isPrefetched =
prefetched[href + '%' + as + (curLocale ? '%' + curLocale : '')]
if (shouldPrefetch && !isPrefetched) {
prefetch(router, href, as, {
locale: curLocale,
})
}
}, [as, href, isVisible, locale, p, router])
}, [as, href, isVisible, locale, p, pagesRouter?.locale, router])

const childProps: {
onTouchStart: React.TouchEventHandler
Expand Down Expand Up @@ -495,7 +520,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
shallow,
scroll,
locale,
Boolean(appRouter),
isAppRouter,
p
)
}
Expand All @@ -513,7 +538,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
}

// Check for not prefetch disabled in page using appRouter
if (!(!p && appRouter)) {
if (!(!p && isAppRouter)) {
if (isLocalURL(href)) {
prefetch(router, href, as, { priority: true })
}
Expand All @@ -533,7 +558,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
}

// Check for not prefetch disabled in page using appRouter
if (!(!p && appRouter)) {
if (!(!p && isAppRouter)) {
if (isLocalURL(href)) {
prefetch(router, href, as, { priority: true })
}
Expand All @@ -549,18 +574,22 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
(child.type === 'a' && !('href' in child.props))
) {
const curLocale =
typeof locale !== 'undefined' ? locale : router && router.locale
typeof locale !== 'undefined' ? locale : pagesRouter?.locale

// we only render domain locales if we are currently on a domain locale
// so that locale links are still visitable in development/preview envs
const localeDomain =
router &&
router.isLocaleDomain &&
getDomainLocale(as, curLocale, router.locales, router.domainLocales)
pagesRouter?.isLocaleDomain &&
getDomainLocale(
as,
curLocale,
pagesRouter?.locales,
pagesRouter?.domainLocales
)

childProps.href =
localeDomain ||
addBasePath(addLocale(as, curLocale, router && router.defaultLocale))
addBasePath(addLocale(as, curLocale, pagesRouter?.defaultLocale))
}

return legacyBehavior ? (
Expand Down
7 changes: 6 additions & 1 deletion packages/next/client/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,12 @@ export default singletonRouter as SingletonRouter
export { default as withRouter } from './with-router'

export function useRouter(): NextRouter {
return React.useContext(RouterContext)
const router = React.useContext(RouterContext)
if (!router) {
throw new Error('invariant expected pages router to be mounted')
}

return router
}

// INTERNAL APIS
Expand Down

0 comments on commit a100813

Please sign in to comment.