Skip to content

Example of overriding the built-in next/link component for Next 15.3+ and React 19.1+ #11

@rothnic

Description

@rothnic
  • We have an existing nextjs app directory project that is already using next/link everywhere and would be hard to apply consistent use of the foresight link
  • I started from an example I found of replacing all next/link imports using tsconfig
  • I was able to get the prefetching working, but due to recent changes to nextjs (15.3.4) and react (19.1.0, ref vs forwardedRef changes), there must be something off. When clicking the prefetched links (you can see with the <link>?_rsc=XXXX network requests), it appears to cause a full page reload. I can't seem to find why it isn't using the cached RSC payload instead.

tsconfig.json

"paths": {
...
"next/link": ["overrides/next/link"]
}
...

The provided forsight link example had to be modified due to how we can't import anything from next/link since that would cause a circular reference. The LinkProps type is not exported from next/dist/client/link.

src/overrides/next/link.tsx

'use client'

import type { ComponentProps } from 'react'
import NextUnifiedLink from 'next/dist/client/link'
import { useRouter } from 'next/navigation'
import useForesight from '@/hooks/useForesight'
import { type ForesightRegisterOptions } from 'js.foresight'

interface ForesightLinkProps
  extends ComponentProps<typeof NextUnifiedLink>,
    Omit<ForesightRegisterOptions, 'element' | 'callback'> {
  children?: React.ReactNode
}

export function ForesightLink({ children, className, ...props }: ForesightLinkProps) {
  const router = useRouter()
  const { elementRef, registerResults } = useForesight<HTMLAnchorElement>({
    callback: () => {
      const hrefString =
        typeof props.href === 'string' ? props.href : (props.href as { pathname: string }).pathname
      router.prefetch(hrefString)
    },
    hitSlop: props.hitSlop,
    name: props.name,
    meta: props.meta,
    reactivateAfter: props.reactivateAfter,
  })

  return (
    <NextUnifiedLink
      {...props}
      prefetch={registerResults?.isTouchDevice ?? false}
      ref={elementRef}
      className={className}
    >
      {children}
    </NextUnifiedLink>
  )
}

export const Link = ForesightLink
export default Link

src/hooks/useForsight.tsx (i had to modify this component so that the example in the docs worked. The docs example requires us to reference registerResults.current?.isTouchDevice, rather than registerResults?.isTouchDevice

import { useEffect, useRef, useState } from 'react'
import {
  ForesightManager,
  type ForesightRegisterOptionsWithoutElement,
  type ForesightRegisterResult,
} from 'js.foresight'

export default function useForesight<T extends HTMLElement = HTMLElement>(
  options: ForesightRegisterOptionsWithoutElement
) {
  const elementRef = useRef<T>(null)
  const [registerResults, setRegisterResults] = useState<ForesightRegisterResult | null>(null)

  useEffect(() => {
    if (!elementRef.current) return

    const result = ForesightManager.instance.register({
      element: elementRef.current,
      ...options,
    })
    setRegisterResults(result)
  }, [options])

  return { elementRef, registerResults }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions