Navigation
Use the <Link> component to navigate between pages. It renders a plain <a> element and accepts all standard anchor attributes alongside a typed to prop:
import { Link } from '$houdini'
<Link to="/shows">All Shows</Link>Type-Safe URLs
Houdini knows every valid route in your app, so to is type-checked at compile time. For parameterized routes, pass a params object and Houdini interpolates it into the URL at render time:
// static route: TypeScript verifies "/shows" is a real page<Link to="/shows">All Shows</Link>
// parameterized route: TypeScript requires params.id<Link to="/shows/[id]" params={{ id: show.id }}> {show.title}</Link>TypeScript will error if:
torefers to a path that doesn’t exist in your appparamsis missing for a parameterized routeparamscontains the wrong keys or incompatible value types
Param types
Param types come from the GraphQL variables attached to that route. If $id is declared as ID!, the param accepts a string (IDs are opaque values, not numbers). Custom scalars from your Houdini config are reflected too. A DateTime param expects a Date value and will serialize into the url with the function you defined in your config file.
External links
External URLs, fragments, relative paths, and other non-app hrefs are accepted without any special handling:
<Link to="https://example.com">External</Link><Link to="mailto:hello@example.com">Email</Link><Link to="#section">Jump to section</Link><Link to="./sibling">Relative</Link>Search Params
The search prop configures the query string of a Link. Pass an object and Houdini will serialize it into the URL:
// → /shows?genre=comedy<Link to="/shows" search={{ genre: 'comedy' }}>Comedies</Link>The route’s nullable query variables show up as typed keys, so a mistyped value (a string where the query wants an Int) is a compile error rather than a silently empty result. search isn’t limited to them, though: extra keys are allowed so the query string can also hold UI-only state that no query reads (a selected tab, an open modal). A list variable accepts an array and serializes as repeated keys:
// → /shows?tag=comedy&tag=drama<Link to="/shows" search={{ tags: ['comedy', 'drama'] }}>Both</Link>A search param backed by a custom scalar is marshaled into the URL the same way a path param is, so a DateTime accepts a Date and serializes with your config’s marshal function. On the read side, useRoute().search unmarshals it back to its runtime type (a Date). One caveat: because the URL is string-only, values are decoded with JSON.parse before unmarshaling, so a custom scalar whose value is "true" or "123" comes back as a boolean or number.
These params aren’t just decoration on the URL. They flow straight back into the route’s query, and changing them re-runs it, so a <Link> that swaps ?genre=comedy for ?genre=drama refetches the page with the new filter. See Search Params for the data side of the story.
goto accepts the same typed target as <Link> (a to route plus its params and search) and builds the URL for you, with the same compile-time checks:
// → /shows?genre=comedygoto({ to: '/shows', search: { genre: 'comedy' } })
// → /shows/123goto({ to: '/shows/[id]', params: { id: show.id } })It also accepts a ready-made URL string as an escape hatch, which works the same way for query strings as it does for paths. Pass the whole URL, search string included:
goto(`/shows?genre=${encodeURIComponent(genre)}`)Disabled
Pass disabled to prevent navigation. The href attribute is omitted so the element is inert, and you can add a class to style it:
<Link to="/[[id]]" params={{ id: id - 1 }} disabled={id <= 1} className={id <= 1 ? 'disabled' : undefined}> previous</Link>Preloading
Add preload to a <Link> and Houdini will begin fetching when the user hovers, before they click:
<Link to="/shows" preload>All Shows</Link>By default this fetches both the page component and its data. You can narrow it:
"data": only the GraphQL data"component": only the JavaScript bundle"page": both (default)
<Link to="/shows" preload="data">All Shows</Link>Imperative Navigation
When you need to navigate in response to something other than a click (after a form submission, inside an effect, or from a callback), call goto from useRoute:
import type { PageRoute } from './$types'import { useRoute } from '$houdini'
export function SearchForm() { const { goto } = useRoute<PageRoute>()
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() const query = new FormData(e.currentTarget).get('q') goto({ to: '/search', search: { q: query as string } }) }
return ( <form onSubmit={handleSubmit}> <input name="q" /> <button type="submit">Search</button> </form> )}import { useRoute } from '$houdini'export function SearchForm() { const { goto } = useRoute() function handleSubmit(e) { e.preventDefault() const query = new FormData(e.currentTarget).get('q') goto({ to: '/search', search: { q: query } }) } return ( <form onSubmit={handleSubmit}> <input name="q" /> <button type="submit">Search</button> </form> )}useRoute also exposes pathname, params, and search if you need to read the current URL, for example to mark a link as active:
import type { PageRoute } from './$types'import { useRoute } from '$houdini'
export function NavLink({ href, label }: { href: string; label: string }) { const { pathname } = useRoute<PageRoute>() return ( <a href={href} aria-current={pathname === href ? 'page' : undefined}> {label} </a> )}See useRoute for the full API.
Loading states
Navigations run as a React transition, so the current page stays on screen while the next route loads; a navigation that resolves quickly swaps straight to the new page with no intermediate flash.
If a navigation stays pending longer than a configurable threshold, Houdini shows the destination route’s @loading state (its skeleton) instead of holding the old page indefinitely. Two config options control the timing:
router.loadingDelay(default:200ms): how long a navigation may stay pending before the loading state is shown. Navigations faster than this never show it.router.minDuration(default:400ms): once the loading state is shown, the minimum time to keep it visible, so a response landing just afterloadingDelaydoesn’t cause a flicker.
export default { // ... router: { loadingDelay: 200, minDuration: 400, },}Queries whose variables don’t change across a navigation aren’t re-fetched, so persistent chrome (for example a layout query that doesn’t depend on the route) keeps its data and never flashes. Only the parts of the page whose data actually changed show their loading state.
A route without an @loading state has nothing to show in its place, so a slow navigation to it simply keeps the previous page on screen until the new one is ready.
Tracking navigation
useNavigation exposes the router’s in-flight navigation so your app can render its own pending UI: a global progress bar, a spinner on the clicked link, or disabling controls while a navigation settles.
import { useNavigation } from '$houdini'
export function ProgressBar() { const { pending, to } = useNavigation() if (!pending) return null return <div className="progress-bar" aria-label={`navigating to ${to}`} />}pending is true from the moment a navigation starts until the destination renders its actual content (including while the destination’s @loading state is showing), and to carries the destination url while pending (null when idle). The returned object also includes goto, the same navigate function useRoute exposes, so navigation chrome only needs this one hook. Note that this is different from useRoute, which describes the route currently rendered: during a navigation, useRoute keeps answering for what’s on screen while useNavigation describes where the router is headed.