Links and Navigation
Use standard HTML <a> tags for navigation. The client router automatically intercepts link clicks and performs client-side navigation, giving you the benefits of a single-page application without needing a special <Link> component or API overhead.
Basic Usage
Just use regular anchor tags:
export default function HomePage() {
return (
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/blog">Blog</a>
</nav>
)
}That's it. No imports, no special components, no learning curve. If you know HTML, you know how to navigate in rari.
How It Works
When you click a link, rari's client router:
- Intercepts the click event on
<a>tags - Prevents the default browser navigation
- Fetches only the new page content (not the entire HTML document)
- Updates the URL in the browser
- Streams the new content into the page
- Preserves layout state and scroll position
This gives you instant navigation with no full-page reloads, while keeping your code simple and standards-based.
Client-Side Navigation Benefits
Layout Preservation
Layouts don't re-render during navigation. If you have a sidebar, header, or any shared UI in a layout component, it stays mounted and preserves its state:
import { useState } from 'react'
import type { LayoutProps } from 'rari'
export default function DashboardLayout({ children }: LayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(true)
return (
<div>
<aside>
{/* This sidebar state persists across page navigation */}
<button onClick={() => setSidebarOpen(!sidebarOpen)}>
Toggle
</button>
<nav>
<a href="/dashboard">Overview</a>
<a href="/dashboard/analytics">Analytics</a>
<a href="/dashboard/settings">Settings</a>
</nav>
</aside>
<main>{children}</main>
</div>
)
}Navigating between /dashboard/analytics and /dashboard/settings only swaps the page content. The sidebar stays mounted with sidebarOpen intact.
Streaming Updates
Only the changed content is fetched and streamed. If you navigate from /blog to /blog/hello-world, rari fetches just the blog post page, not the entire document with layouts and navigation.
Optimized Performance
The router includes built-in optimizations:
- Debounced navigation to prevent rapid-fire requests
- Automatic request cancellation when navigating away
- Route info caching to avoid redundant fetches
- Scroll position restoration on back/forward navigation
Styling Links
Since links are just <a> tags, style them however you want. CSS, Tailwind, CSS modules, styled-components, anything:
export default function Navigation() {
return (
<nav className="flex gap-4">
<a
href="/products"
className="px-4 py-2 text-gray-700 hover:text-gray-900 hover:bg-gray-100 rounded-md transition-colors"
>
Products
</a>
<a
href="/pricing"
className="px-4 py-2 text-white bg-indigo-600 hover:bg-indigo-700 rounded-md transition-colors"
>
Pricing
</a>
</nav>
)
}Active Link Styling
Use the pathname prop available in layouts and pages to style active links:
import type { LayoutProps } from 'rari'
export default function RootLayout({ children, pathname }: LayoutProps) {
const isActive = (path: string) => pathname === path
return (
<div>
<nav>
<a
href="/"
className={isActive('/') ? 'text-indigo-600 font-semibold' : 'text-gray-700'}
>
Home
</a>
<a
href="/about"
className={isActive('/about') ? 'text-indigo-600 font-semibold' : 'text-gray-700'}
>
About
</a>
</nav>
<main>{children}</main>
</div>
)
}For more complex active state logic, you can check if the pathname starts with a certain path:
const isActiveSection = (path: string) => pathname?.startsWith(path)
<a
href="/docs/getting-started"
className={isActiveSection('/docs') ? 'active' : ''}
>
Documentation
</a>External Links
External links work exactly as you'd expect. They're not intercepted and perform normal browser navigation:
export default function Footer() {
return (
<footer>
<a href="https://github.com/your-repo" target="_blank" rel="noopener noreferrer">
GitHub
</a>
<a href="https://twitter.com/your-handle" target="_blank" rel="noopener noreferrer">
Twitter
</a>
</footer>
)
}The router automatically detects external URLs and lets the browser handle them normally.
Special Link Behaviors
Hash Links
Hash links scroll to elements on the current page:
<a href="#features">Jump to Features</a>
<section id="features">
<h2>Features</h2>
</section>The router intercepts hash links and performs smooth scrolling to the target element.
Download Links
Links with the download attribute are not intercepted:
<a href="/files/report.pdf" download>
Download Report
</a>Modified Clicks
Links clicked with modifier keys (Ctrl, Cmd, Shift, Alt) are not intercepted, allowing users to open links in new tabs or windows as expected:
Cmd/Ctrl + Click: Opens in new tabShift + Click: Opens in new windowAlt + Click: Downloads the link
Target Attribute
Links with target attributes other than _self are not intercepted:
<a href="/admin" target="_blank">
Open Admin in New Tab
</a>Programmatic Navigation
Sometimes you need to navigate in response to events, form submissions, or conditional logic, not just link clicks. rari provides two ways to do this.
The navigate Function
For navigation from non-component code (e.g., event handlers, utilities, or server/standalone scripts), import navigate directly. Inside React components, prefer useRouter for in-component navigation:
'use client'
import { navigate } from 'rari/router'
export default function SearchForm() {
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const query = formData.get('q') as string
await navigate(`/search?q=`)
}
return (
<form onSubmit={handleSubmit}>
<input name="q" placeholder="Search..." />
<button type="submit">Search</button>
</form>
)
}navigate accepts an optional second argument with navigation options:
// Replace the current history entry instead of pushing
await navigate('/dashboard', { replace: true })The navigate function is client-side only. If called before the router initializes (during early hydration), it falls back to window.location automatically. When using the fallback, the replace option is respected by calling window.location.replace(...) instead of assigning window.location.href.
The useRouter Hook
For React components that need full router access, useRouter gives you navigation methods plus reactive route state:
'use client'
import { useRouter } from 'rari/router'
export default function LogoutButton() {
const router = useRouter()
async function handleLogout() {
await fetch('/api/logout', { method: 'POST' })
await router.push('/login')
}
return <button onClick={handleLogout}>Log out</button>
}The hook returns:
| Method / Property | Description |
|---|---|
push(href, options?) | Navigate to a new URL (adds history entry) |
replace(href, options?) | Navigate without adding a history entry |
back() | Go back (equivalent to browser back button) |
forward() | Go forward |
refresh() | Re-render the current route |
prefetch(href) | Prefetch a route for faster future navigation |
pathname | Current pathname (reactive) |
params | Current route parameters (reactive) |
searchParams | Current URL search params (reactive) |
Navigation Options
Both navigate and router.push/router.replace accept the same options:
| Option | Type | Default | Description |
|---|---|---|---|
replace | boolean | false | Replace the current history entry instead of pushing |
Convenience Hooks
If you only need a specific piece of route state, use the focused hooks to avoid unnecessary re-renders:
'use client'
import { usePathname, useParams, useSearchParams } from 'rari/router'
function Breadcrumb() {
const pathname = usePathname()
// ...
}
function ProductPage() {
const params = useParams() // { slug: 'my-product' }
// ...
}
function FilteredList() {
const searchParams = useSearchParams() // URLSearchParams
const sort = searchParams.get('sort')
// ...
}When to Use What
<a>tags: the default. Use for all standard navigation.navigate(): for navigation outside React components, or when you don't need reactive route state.useRouter(): for components that need to navigate and read route state.
Prefetching
You can prefetch routes programmatically using the useRouter hook:
'use client'
import { useRouter } from 'rari/router'
export default function ProductCard({ href }: { href: string }) {
const router = useRouter()
return (
<div onMouseEnter={() => router.prefetch(href)}>
<a href={href}>View Product</a>
</div>
)
}Automatic link prefetching is not built in. The streaming architecture and route caching keep navigation fast enough that it hasn't been necessary. This may change in a future release based on feedback.
Why No Link Component?
The decision to use standard <a> tags instead of a custom component is intentional:
- Zero learning curve: If you know HTML, you already know how to navigate
- Better DX: No imports, no prop mapping, no framework-specific APIs
- Standards-based: Works with any styling solution, accessibility tools, and browser features
- Less code: Fewer abstractions mean less to maintain and debug
- Progressive enhancement: Links work even if JavaScript fails to load
The client router handles all the complexity behind the scenes, giving you SPA-like navigation with the simplicity of static HTML.