Skip to content

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:

  • to refers to a path that doesn’t exist in your app
  • params is missing for a parameterized route
  • params contains 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 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=comedy
goto({ to: '/shows', search: { genre: 'comedy' } })
// → /shows/123
goto({ 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:

src/routes/search/+page.tsx
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>
)
}
src/routes/search/+page.jsx
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.