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.
SearchParamsUpdater | what updateSearchParams takes: an object of the new query, or a function of the current one |
HashTagsUpdater | the same for hash tags — a HashTag[], or a function of the current ones |
StateUpdater | a 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.
push or <Link>?
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
- Nested outlets — routes inside routes.