Ramonda

Navigating

Navigator lets a component navigate from code:

import { Navigator } from "./routes";

export class Toolbar extends Component {
  route = this.use(Navigator);

  // A method, not an inline arrow: a handler built during render is re-attached to
  // the element on every render, and development builds report it (RMD020).
  showPlayer() {
    this.route.push("/players/9");
  }

  render() {
    return (
      <nav>
        <button onclick={this.route.back}>← Back</button>
        <button onclick={this.showPlayer}>Player 9</button>
      </nav>
    );
  }
}
push(href, opts?)go to a URL, adding a history entry
replace(href, opts?)go there without adding one
updateSearchParams(next, opts?)change only the query — see keeping state in the URL
updateHashTags(next, opts?)change only the hash
back() / forward()move through history

The types — NavigateOptions and the updaters

The second argument to push and replace is NavigateOptions: replace to leave no history entry, and scroll, which is the one knob. The partial updaters take PartialNavigateOptions instead — the same two fields with a different default for scroll, because those are in-place edits rather than a move to another page.

SearchParamsUpdaterwhat updateSearchParams takes: an object of the new query, or a function of the current one
HashTagsUpdaterthe same for hash tags — a HashTag[], or a function of the current ones
StateUpdatera whole RouterState from the previous one, for the rare change that touches several parts at once

The function forms exist to be race-free. An object says what the query should BE; a function says what it should be given what it is now, which is the form that survives two updates in the same tick.

RouterState is the whole URL structured — baseUrl, queryParams as a flat record, and hashTags as ordered HashTags with a key, a value and a level. It is the source of truth at runtime: the URL is rebuilt from it, and read back only at startup and on back/forward.

Scrolling is your choice, per call

A push or replace moves to another page, so it scrolls to the top by default. Pass { scroll: false } to stay where you are — useful when a <RouteOutlet> sits partway down a long page and you don't want the jump. The in-place updaters (updateSearchParams, updateHashTags) are the opposite: they don't scroll unless you ask with { scroll: true }.

The methods are already bound

onclick={this.route.back} works as-is — the hook's methods are bound to it, so passing one as a handler keeps working. No () => this.route.back() wrapper needed.

Use <Link> when the thing is a link — someone should be able to middle-click it and a crawler should follow it. Use push when the navigation is the result of something else: a submitted form, a resolved choice, a redirect after a save.

The Router hook can do this too

The component that mounts the router with this.use(Router) doesn't need a separate Navigator to read the URL or navigate — the Router instance exposes the same pathname, searchParams, hashTags, push, replace, updateSearchParams, updateHashTags, back and forward, on top of the setup work it does. It's the same surface, from the piece that owns the state:

export class App extends Component {
  router = this.use(Router);

  goHome() {
    this.router.push("/");
  }

  render() {
    return (
      <div className="app">
        <button onclick={this.goHome}>{this.router.pathname}</button>
        <RouteOutlet routes={routes} />
      </div>
    );
  }
}

The one thing it can't give you is params() — those are matched by a <RouteOutlet>, which sits below the Router, so only a Navigator inside a routed page has them.

There is no global router

You can't import the router and call push from just anywhere — navigation is reachable only from inside the tree, through Navigator. (A module-level router would be shared by every request on a server, so one visitor's navigation could show up for another.) If a plain function needs to navigate, pass it a callback — the component calling it has the hook.

Next