Skip to content

Client navigation

<Link> keeps progressive enhancement: before hydration it is an anchor, and after hydration a plain in-app click fetches page data and swaps the page tree. Use navigateTo() for a path, href() to resolve a named route, and refresh() to fetch the current App, Layout, and Page loaders again without pushing history.

useIsNavigating() returns the router’s current boolean state. It is true through a client navigation, refresh(), or a locale change, and false during SSR. A component mounted after a transition starts reads the current state immediately; there is no delay. When a newer transition supersedes an earlier one, completion of the earlier work cannot clear the newer pending state.

Use it for a pending indicator, not a percent-complete claim:

src/web/components/navigation-progress.tsx
import { useIsNavigating } from "@warlock.js/web";
import "./navigation-progress.css";
export function NavigationProgress() {
const isNavigating = useIsNavigating();
return (
<div
aria-hidden={!isNavigating}
aria-label="Loading next page"
className={isNavigating ? "navigation-progress navigation-progress--active" : "navigation-progress"}
/>
);
}
src/web/components/navigation-progress.css
.navigation-progress {
position: fixed;
inset: 0 0 auto;
height: 3px;
transform: scaleX(0);
transform-origin: left;
}
.navigation-progress--active {
transform: scaleX(0.7);
transition: transform 160ms ease-out;
}

The boolean means that router work is active. It does not expose network or rendering percentage, so do not turn it into a numeric progress meter.

routerEvents remains available for analytics and post-navigation observation. Use useIsNavigating() for reactive UI state; do not infer pending state from events, because an event callback can miss a transition that began before the component subscribed.

import { apiHref, href, navigateTo, refresh } from "@warlock.js/web";
navigateTo({ name: "products.details", params: { id: "42" }, query: { tab: "reviews" } });
navigateTo("/cart", { replace: true });
const productUrl = href("products.details", { params: { id: "42" }, query: { tab: "reviews" } });
const reviewsEndpoint = apiHref("products.reviews", { params: { id: "42" }, query: { page: 2 } });
await refresh();

navigateTo() accepts a path or a { name, params, query } route target. Before the client runtime is connected (for example, a click before hydration finishes), it falls back to a real browser navigation, so you never need to write window.location.assign yourself. It returns false only on the server, or when the runtime declines a same-origin URL.

href() resolves named page routes through the server-published route table. It takes either positional arguments (href(name, params, query)) or an object (href(name, { params, query })). apiHref() does the same for named API routes, which is useful for fetch URLs. Generated declarations in .warlock/typings/web-routes.d.ts make page names and required path parameters type-safe when they are available; runtime validation remains active before generation. refresh() leaves history unchanged and resolves false when fresh data did not reach the screen.

LinkProps is exported from @warlock.js/web when a shared component should accept the exact same destination contract as <Link>:

import { Link, type LinkProps } from "@warlock.js/web";
export function NavigationItem(props: LinkProps) {
return <Link {...props} />;
}

This keeps generated named-route and parameter checking intact. Do not redeclare destination props as broad strings in a wrapper.