Client navigation
Use primate/client to navigate from browser code without a full page reload.
It is useful for interactions that happen outside a link, such as a desktop
notification click.
import client from "primate/client";
await client.navigate("/projects/42");client.navigate() is available only in browser code. Call it from a hydrated
frontend component, event handler, or other client-side module; do not call it
while rendering on the server.
Destinations
Pass either a relative or absolute URL:
await client.navigate("projects/42");
await client.navigate("/projects/42?tab=activity");
await client.navigate("https://app.example.com/projects/42");Relative URLs resolve from the current page. Same-origin destinations use Primate's SPA navigation: Primate fetches and updates the current frontend without a full document reload, updates browser history, and scrolls to a hash target when one is present.
External destinations use normal browser navigation instead:
await client.navigate("https://example.com");This lets application code use the same method without checking whether a URL belongs to the current app.
Notification example
For example, navigate when a notification is clicked:
import client from "primate/client";
notification.addEventListener("click", () => {
void client.navigate(`/projects/${project.id}`);
});Use void when the caller does not need to wait for the navigation to finish.
Refreshing the current route
Use refresh to fetch fresh server data for the current pathname and query
without changing the URL, adding a history entry, scrolling, or reloading the
document:
import client from "primate/client";
const refreshed = await client.refresh();The promise resolves to true after Primate applies the fresh route data and
dispatches the navigation completion event. It resolves to false if the
request fails, is superseded by another navigation, redirects to another URL,
or does not return a successful Primate route response. In those cases, the
current view, URL, and history remain unchanged.
refresh is distinct from navigating to the current URL. A same-URL
client.navigate() call keeps its existing no-op behavior.
Links and events
Primate automatically intercepts ordinary same-origin links after hydration, so
you usually do not need to call client.navigate() from an anchor's click
handler. For custom link handling, pass the click event as the optional second
argument:
link.addEventListener("click", event => {
void client.navigate(link.href, event);
});For same-origin URLs, the event is prevented and the SPA navigation runs. For external URLs, the event is left untouched so the browser performs its normal link navigation.
Native forms
After hydration, Primate also enhances ordinary same-origin forms that target the current window. GET submissions encode successful controls in the query string without sending a request body. As in a native submission, this replaces any query string written directly in the form action. Repeated controls and the button that initiated submission are preserved.
Submit-button overrides such as formaction, formmethod, and formenctype
are honored. Forms with another target, an external action, or an unsupported
encoding remain under browser control. A handler that calls preventDefault()
also opts that submission out of Primate's enhancement.
Frontend page responses are rendered even when they use an error status such as
400 or 422, allowing a route to display validation errors without a full-page
load. JSON API responses are not treated as frontend pages; use fetch when an
application needs to consume an API payload directly.
Navigation completion
Use client.onNavigation when application-wide browser behavior must run after
Primate has mounted or updated a destination view. At that point the listener
observes both the destination URL and its DOM:
import client from "primate/client";
const unsubscribe = client.onNavigation(({ source, url }) => {
analytics.page(url, { source });
});
// Stop listening when the owning integration is disposed.
unsubscribe();source is "navigate" for enhanced links and client.navigate(), "form"
for enhanced form navigation, "history" for Back and Forward, or "refresh"
for client.refresh(). A successful navigation invokes listeners exactly once.
Failed, superseded, and full-document navigation does not invoke them.
Hash-only and shallow navigation do not invoke listeners because they do not mount or update a destination view. Back and Forward between shallow states likewise do not invoke them. Prefer your frontend's component lifecycle hooks for behavior local to one component.
Query strings and hashes
Query strings and hashes are preserved:
await client.navigate("/search?q=primate#results");Navigating to a different query string on the current route performs an SPA navigation. Navigating to a hash on the current route updates history and scrolls to the matching element.
Same-origin route redirects preserve the complete destination, including its query string and fragment. Primate updates history with that destination and scrolls after the redirected view mounts. This applies to enhanced links and forms, including redirect chains. Cross-origin or opaque redirects are left to the browser.
Shallow navigation
Use shallow to update the query string without fetching, remounting the
current view, or scrolling:
await client.navigate("/search?q=primate", { shallow: true });The public request store updates immediately. Back and Forward update it again
without fetching while traversal remains within the same rendered view. Use
replace when the new URL should replace the current history entry:
await client.navigate("/search?q=framework", {
replace: true,
shallow: true,
});Shallow navigation applies only when the destination has the current pathname. A different pathname performs normal SPA navigation.
See Frontends for frontend setup and the available hydrated frontend integrations.