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