Link does not reload the page. It asks the server for the part of the tree
that actually changed, and swaps it in.
import Link from '@rsc-kit/core/Link'
<Link href="/about">About</Link>What a navigation actually sends
Going from /posts/one to /posts/two, the root layout and the posts layout
are already mounted and identical. The client says which layouts it holds, the
server compares that with the new route’s chain, and answers with the deepest
piece that differs — the page alone.
You get this for free. There is nothing to configure and no loading prop to
thread.
Two things follow from it, and they are the reason it works this way:
State above the swap survives. A sidebar’s scroll position, an open menu, a half-typed form in a layout — none of it is touched, because none of it was re-rendered.
A page you return to is still there. Recently visited pages are kept mounted and hidden rather than thrown away, so going back restores a half-filled form exactly as you left it. Only a handful are kept, ordered by last visit.
Programmatic navigation
'use client'
import { visit, prefetch } from '@rsc-kit/core/router'
<button onClick={() => visit('/checkout')}>Checkout</button>Links prefetch on hover already. prefetch is for when you know where someone
is going before they hover:
useEffect(() => { prefetch('/step-2') }, [])Showing progress
'use client'
import { useLinkStatus } from '@rsc-kit/core/useLinkStatus'
function Spinner() {
const { pending } = useLinkStatus()
return pending ? <span>Loading…</span> : null
}A loading.tsx beside a page is shown while that page’s data resolves:
export default function Loading() {
return <p>Loading posts…</p>
}Refreshing the current page
'use client'
import { refresh } from '@rsc-kit/core/router'
<button onClick={() => refresh()}>Reload</button>Scroll positions are restored afterwards — the window’s and any element with its own overflow, such as a sidebar.