diff --git a/.changeset/link-prefetch-on-hover.md b/.changeset/link-prefetch-on-hover.md new file mode 100644 index 00000000..a5f15434 --- /dev/null +++ b/.changeset/link-prefetch-on-hover.md @@ -0,0 +1,5 @@ +--- +'spiceflow': patch +--- + +Add RSC prefetching on Link hover. The `Link` component now fetches the RSC payload when the user hovers (with 80ms debounce), focuses, or touches a link. Cached responses are used on navigation, making client-side page transitions feel instant. Prefetch is enabled by default and can be disabled with ``. A `prefetchRoute(href)` function is also exported from `spiceflow/react` for programmatic use. diff --git a/spiceflow/src/react/components.tsx b/spiceflow/src/react/components.tsx index 47ee30f6..09ceffe6 100644 --- a/spiceflow/src/react/components.tsx +++ b/spiceflow/src/react/components.tsx @@ -7,6 +7,7 @@ import { ServerPayload } from '../spiceflow.js' import { isRedirectError, isNotFoundError, getErrorContext, contextHeaders } from './errors.js' import { useFlightData } from './context.js' import { ProgressBar } from './progress.js' +import { prefetchRoute } from './prefetch.js' export function LayoutContent(props: { id?: string }) { const data = useFlightData() @@ -170,10 +171,41 @@ export function DefaultGlobalErrorPage(props: ErrorPageProps) { ) } -export function Link(props: React.ComponentPropsWithRef<'a'>) { +export function Link({ + prefetch = true, + ...props +}: React.ComponentPropsWithRef<'a'> & { prefetch?: boolean }) { + const hoverTimer = React.useRef | null>(null) + return ( { + props.onMouseEnter?.(e) + if (!prefetch) return + const href = e.currentTarget.href + if (!href) return + hoverTimer.current = setTimeout(() => prefetchRoute(href), 80) + }} + onMouseLeave={(e) => { + props.onMouseLeave?.(e) + if (hoverTimer.current) { + clearTimeout(hoverTimer.current) + hoverTimer.current = null + } + }} + onFocus={(e) => { + props.onFocus?.(e) + if (!prefetch) return + const href = e.currentTarget.href + if (href) prefetchRoute(href) + }} + onTouchStart={(e) => { + props.onTouchStart?.(e) + if (!prefetch) return + const href = e.currentTarget.href + if (href) prefetchRoute(href) + }} onClick={(e) => { if ( e.metaKey || @@ -186,7 +218,6 @@ export function Link(props: React.ComponentPropsWithRef<'a'>) { return } e.preventDefault() - props.onClick?.(e) router.push(e.currentTarget.href) }} diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index d77e5daa..7f4b35f0 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -25,6 +25,7 @@ import { isDeploymentMismatchResponse, } from './deployment.js' import { getErrorContext } from './errors.js' +import { consumePrefetch } from './prefetch.js' // Reads the RSC flight payload that the server injected as