Primate LogoPrimate

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.

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.

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.

Previous
Intro
Next
Angular