GioJSdocs
On this page

Linking & Navigating

Client-side navigation, router hooks, prefetching, scroll and focus.

Use GioLink for internal navigation. It prefetches on hover intent by default and swaps content without a full reload. Set prefetch="viewport" to instead prefetch once when the link scrolls into view (via IntersectionObserver), or prefetch={false} to disable prefetching.

tsx
import { GioLink } from '@gio.js/react';

<GioLink href="/about">About</GioLink>
<GioLink href="/posts/1" prefetch="viewport">First post</GioLink>
<GioLink href="/search?q=gio" replace scroll={false}>Search</GioLink>
<GioLink href="#comments">Jump to comments</GioLink>

How a soft navigation works

A click fetches the next page's HTML (or takes a fresh prefetch), loads the route's client chunk and any of its stylesheets the page does not have yet (waiting at most 3 seconds for them), and renders the new page into the same React root. Layouts the two pages share stay mounted, so their state (an open sidebar, a search box, a playing video) survives the navigation; the page itself mounts fresh, also when only a dynamic segment changes (/posts/1 to /posts/2). So does a layout inside a dynamic segment when that segment's value changes: app/teams/[team]/layout.tsx mounts fresh from /teams/a to /teams/b, and keeps its state between the pages of one team. An error a folder's error.tsx boundary caught is cleared by the next navigation, also one that only changes the query (?q=bad to ?q=good).

Only GioJS pages are rendered in place - HTML with the page boundary, including your not-found.tsx and error.tsx pages. Anything else falls back to a normal full page load, so it shows with its real status: a non-HTML response, a 503 from the server, a network error, a static host's 404.html, or a link to another origin. After a redirect, the address bar and the router hooks show the URL the redirect landed on. When a new deployment went live since the page loaded, the navigation becomes a full load of the new build (the router sends the page's deployment id with every request; a prefetch that finds a new deployment never reloads the page, the click does). A blue/green or rolling setup that keeps the old build's chunks reachable can turn that off with [server] skew_protection = false: the id is then ignored and old pages keep navigating softly.

<GioLink> navigates softly - and prefetches - only for an href that starts with / (a path on your own origin) or with # (a jump on the same page). Anything else is an ordinary link: a full page load with no prefetch. That includes a query-only href="?page=2", a relative href="about" and an absolute URL, even one on your own origin - write /search?page=2 or /blog/about instead. router.push() and navigate() resolve their argument against the current URL first, so router.push('?page=2') does navigate softly.

The root layout (app/layout.tsx) is server-only HTML: a soft navigation does not re-render it, so anything it derives from the URL (an active nav link) keeps the value of the page that was loaded in full. Put URL-dependent UI in a component below it - for a layout shared by every page, use a route group such as app/(site)/layout.tsx.

Router hooks

usePathname, useParams and useSearchParams read the page the router matched. They work during server rendering - in the root layout too - and return the same values when the page hydrates, so they never cause a hydration mismatch; after a soft navigation they return the new page's values.

tsx
import { usePathname, useParams, useSearchParams } from '@gio.js/react';

export default function PostPage() {
  const pathname = usePathname();               // '/posts/42'
  const { id } = useParams<'/posts/:id'>();      // typed from your routes
  const searchParams = useSearchParams();       // read-only URLSearchParams
  const tab = searchParams.get('tab') ?? 'overview';
  // ...
}
  • usePathname() is the path the page was rendered for, without query or hash. With i18n the locale prefix is not part of it (/fr/about gives /about; combine it with useLocale()), and after a [[rewrites]] rule it is the rewritten path.
  • useParams() returns the dynamic segment values. Pass a route pattern (useParams<'/posts/:id'>()) for typed params from the generated typed routes, or a shape (useParams<{ id: string }>()). In a not-found.tsx or error.tsx page and its layouts it returns the params of the route that was not found or failed - the ones their generateMetadata gets - and {} for a URL no route matches.
  • useSearchParams() returns the query as a read-only URLSearchParams: set, append, delete and sort throw. To change the query, navigate. A query key that appears more than once keeps one value (the last), and the order of the keys is not kept: the server hands the query to the page as a map. In a static export it is always empty - each page is rendered once, for no query - so read window.location.search in an effect there.
  • The path and the params are not decoded beyond what the server normalizes: escapes of unreserved characters (%41) become the character, everything else stays percent-encoded - /blog/caf%C3%A9 gives a pathname of /blog/caf%C3%A9 and a slug param of caf%C3%A9. Call decodeURIComponent() on a param to show it.
  • useLocale() returns the request locale, during server rendering too, so <LocaleLink> renders its prefixed href in the server HTML.
tsx
import { useRouter, href } from '@gio.js/react';

function SaveButton({ id }: { id: string }) {
  const router = useRouter();
  async function save() {
    await fetch('/api/posts', { method: 'POST', body: '...' });
    router.push(href('/posts/:id', { id }));
  }
  return <button onClick={save}>Save</button>;
}
MethodWhat it does
push(href, { scroll? })Soft-navigate, adding a history entry.
replace(href, { scroll? })Soft-navigate, replacing the current entry.
back() / forward()Move through history; the router renders the page and restores its scroll position.
refresh()Re-fetch the current page, bypassing (and clearing) the prefetch cache, and re-render it in place: same URL, same scroll position, component state kept, fresh props.
prefetch(href)Fetch a page into the prefetch cache ahead of a navigation.

useRouter() returns the same object on every render, so it is safe in effect dependencies; outside components, navigate(href, { replace, scroll }) does the same as push/replace. All of them return a promise that settles once the new page is on screen, accept any same-origin href (build typed ones with href()), turn other origins into full page loads, refuse javascript: URLs, and do nothing during server rendering.

Scroll

  • A navigation scrolls to the top of the new page, or to the element its #hash names. Pass scroll={false} (on GioLink or to push/replace) to keep the position - handy for tabs or filters that only change the query.
  • Back and forward restore each page's scroll position once that page has rendered - also for entries a plain <a href="#note"> created. The router keeps the position of the current entry as you scroll and takes over history.scrollRestoration while it is active; full page loads and reloads keep the browser's own restoration.
  • Links to a hash on the current page (#comments, /docs#install while on /docs) only scroll: nothing is fetched. href="#" scrolls to the top, as in the browser.

Prefetching

Prefetched pages are kept for 30 seconds (PREFETCH_TTL_MS), at most 50 of them; an older entry is fetched again when it is used. The cache is cleared by router.refresh() and by any non-GET fetch() to your own origin (an API mutation), so a navigation after a change never shows a page prefetched before it. Hovering a link again within those 30 seconds never refetches it, whatever the answer was. A link whose prefetch got a page that cannot be rendered in place (JSON, a page without GioJS) goes straight to a full page load when clicked; a prefetch that failed - a network error, or any non-2xx status such as the 429 the server answers once a client's prefetch budget is spent - never decides the click: the navigation fetches the page itself.

Prefetching is budgeted by the Rust prefetch manager, so a page full of links will not flood your server: each client may have 5 prefetches in flight and start 20 per second, set by [prefetch] max_concurrent and max_per_second in gio.toml, where 0 lifts that budget. [prefetch] enabled = false turns prefetching off site-wide: every prefetch gets 429, and a click still navigates (see Configuration).

Focus and announcements

After a soft navigation to another page, focus moves to the new page's <main> (or the page container when there is none), so keyboard and screen-reader users start at the new content rather than on a link that may be gone, and the new page's title (or its first <h1>) is announced through a visually hidden live region. Give every page a meaningful title. Focus stays where it is when the page focused something itself (an autoFocus input), when it is in a text field that is still on the page - so search-as-you-type with router.replace('?q=' + value) keeps typing in the field - and when a scroll={false} navigation (tabs) leaves the focused element in place. A navigation that only changes the query (filters, sorting, ?page=2) announces nothing and moves focus only if the element that had it is gone.

View transitions

Set a transition preset to animate between pages using the View Transitions API.

tsx
<GioLink href="/about" transition="fade">About</GioLink>

router.push('/about', { transition: 'slide-left' });

Typed routes

The href() helper builds URLs from your route patterns with full type checking. At every server start GioJS generates .gio/routes.d.ts from the discovered routes; the file fills the global GioJS.RegisteredRoutes registry (declaration merging), so patterns autocomplete and params typecheck with zero annotations in your code. The same registry types params in useParams() and in the @gio.js/core types - PageProps<'/posts/:id'>, GetServerSideProps<Props, '/posts/:id'>, GioRequest<'/api/posts/:id'> (see Functions).

tsx
import { GioLink, href } from '@gio.js/react';

// '/posts/:id' autocompletes from your app/ directory.
// A wrong pattern or a missing/misspelled param is a type error.
<GioLink href={href('/posts/:id', { id: post.id })}>{post.title}</GioLink>

href('/about');                        // static routes take no params
href('/docs/*rest', { rest: 'a/b' });  // catch-all keeps its slashes
href('/shop/*path?');                  // optional catch-all: '/shop'
href('/shop/*path?', { path: 'a/b' }); // '/shop/a/b'

Patterns are the URL shapes of your routes, so route groups never appear in them: [id] is :id, [...slug] is *slug, and [[...slug]] is *slug?. Param values are URL-encoded per segment (a catch-all value keeps its / separators); an empty or omitted optional catch-all drops its segment entirely. Projects scaffolded by create-giojs already include the generated file in their tsconfig; in an existing project, add ".gio/routes.d.ts" to the include array of tsconfig.json.