Skip to content

Commit 9db9241

Browse files
docs: rewrite Solid Router navigation as a teaching page
Opens from the design choice (links are anchors, the URL is the API), ties paths, useNavigate, location, search params, and link state to the store example, explains data-pending as the visible half of a held update, fixes the typed search schema to transform the string value, and adds a common-problems section. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6c82d65 commit 9db9241

1 file changed

Lines changed: 139 additions & 104 deletions

File tree

Lines changed: 139 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,155 +1,176 @@
11
---
22
title: "Navigation and typed paths"
33
version: "2.0"
4-
description: "Build typed URLs, navigate with anchors or code, and read the active Solid Router location."
4+
description: "Link between pages with plain anchors, build URLs the compiler checks, navigate from code, read the location and search parameters, show active and pending links, and guard against leaving a page."
55
---
66

7-
Solid Router uses regular anchors for declarative navigation.
8-
Same-origin anchors inside the router base are intercepted through delegated events.
7+
There is no `<Link>` component in Solid Router.
8+
A link is an `<a>`, and the router turns clicks on same-origin anchors inside it into client-side navigations.
9+
That choice shapes this whole page: the URL is the API, and the router's job is to help you build URLs that are correct and react to the one the user is on.
10+
11+
The examples continue the store from the [introduction](/routing/solid-router).
12+
13+
## Links are anchors
914

1015
```tsx
1116
<nav>
12-
<a href={Router.paths()}>Home</a>
13-
<a href={Router.paths.users(42)}>User</a>
14-
<a href="https://example.com">External site</a>
17+
<a href={paths()}>Store</a>
18+
<a href={paths.products("mug")}>Featured</a>
19+
<a href="https://github.com/solidjs/solid">Source</a>
1520
</nav>
1621
```
1722

18-
The router does not intercept these anchors:
23+
The first two are handled by the router; the third is external and the browser handles it as usual.
24+
The router also leaves an anchor alone when it has a `target`, `rel="external"`, a `download` attribute, or a non-HTTP scheme.
25+
Anything a browser would treat as "leave this page" still does.
1926

20-
- External URLs
21-
- Non-HTTP schemes
22-
- Downloads
23-
- Anchors with `rel="external"`
24-
- Anchors with a `target`
27+
Because they are anchors, links work before JavaScript loads, in reader modes, and when opened in a new tab, and every accessibility tool already knows what they are.
2528

26-
The browser handles these anchors.
29+
A few attributes adjust how the router treats one link:
2730

28-
## Build URLs with `Router.paths`
31+
- `replace` replaces the current history entry instead of pushing.
32+
- `noscroll` keeps the scroll position after navigation.
33+
- `state` supplies a JSON value for `location.state` on the destination.
34+
- `preload="false"` skips the route-data preload on hover for this link; the code chunk still warms.
35+
- `link` marks an anchor for the router when `explicitLinks: true` is set, in apps where most anchors should stay full page loads.
2936

30-
The router infers the instance's `paths` proxy from the literal route tree.
31-
Property access adds static segments, and calls bind route parameters.
37+
## Build URLs with `paths`
3238

33-
```tsx
34-
Router.paths();
35-
// "/"
39+
Writing `"/products/" + id` works until a route moves.
40+
`paths` is a proxy inferred from the route tree, so a URL that does not exist is a compile error:
3641

37-
Router.paths.users(42).settings();
38-
// "/users/42/settings"
39-
40-
Router.paths.search({ q: "solid", page: 2 }, "results");
41-
// "/search?q=solid&page=2#results"
42+
```tsx
43+
paths(); // "/"
44+
paths.products("mug"); // "/products/mug"
45+
paths.account.orders(42); // "/account/orders/42"
46+
paths.search({ q: "mug", page: 2 }, "results"); // "/search?q=mug&page=2#results"
4247
```
4348

44-
Every path node converts to a string when used as an `href`, navigation target, or redirect target.
45-
Call a node with no arguments when an API requires a plain `string`.
49+
Property access adds a static segment; a call binds the parameters of that segment.
50+
After the parameters come an optional search object and an optional hash string, mirroring the anatomy of a URL.
4651

47-
`matchFilters` can narrow path-builder inputs.
48-
For example, `{ id: int }` makes `paths.users(id)` accept a number while the route component receives `params.id` as a string.
52+
Every node converts to a string when it lands in an `href`, `navigate()`, or `redirect()`, so a static route is written `paths.account`, not `paths.account()`.
53+
Call a node with no arguments only when an API insists on a plain `string`.
4954

50-
A route `search` schema changes the search object accepted by the corresponding path end.
51-
See [Typed search parameters](#type-search-parameters) for parsed reads.
55+
The types follow the route definitions:
56+
57+
- `matchFilters: { id: int }` makes `paths.products(id)` accept a number.
58+
The component still receives `params.id` as a string, since that is what a URL holds.
59+
- A route `search` schema types the search object the path end accepts, covered [below](#type-search-parameters).
5260

5361
## Navigate from code
5462

55-
`useNavigate` returns the navigator for the current router session.
63+
When the navigation is a link in the interface, use an anchor.
64+
When it is a consequence of something else, such as a saved form or a timeout, use `useNavigate`:
5665

5766
```tsx
5867
import { useNavigate } from "@solidjs/router";
59-
import { Router } from "../router";
68+
import { paths } from "../router";
6069

61-
function SaveButton() {
70+
function CheckoutButton() {
6271
const navigate = useNavigate();
6372

6473
return (
65-
<button onClick={() => navigate(Router.paths.account, { replace: true })}>
66-
Open account
74+
<button onClick={() => navigate(paths.checkout, { replace: true })}>
75+
Check out
6776
</button>
6877
);
6978
}
7079
```
7180

72-
Navigation options control:
73-
74-
- Path resolution
75-
- History replacement
76-
- Scrolling
77-
- Location state
81+
The options are the same as the link attributes: `replace`, `scroll`, and `state`, plus `resolve` for how a relative string is interpreted.
82+
A string starting with `/` resolves under the router's `base`; other strings resolve against the current location like a relative URL.
83+
A number moves through history: `navigate(-1)` is back.
7884

79-
Relative strings resolve as URLs against the current location.
80-
Leading `/` strings resolve under the router base.
81-
Pass a number to move through history, such as `navigate(-1)`.
85+
Inside a route component you can also navigate with `redirect()` from a server function or action; the [Data](/routing/solid-router/data#what-revalidates-after-a-mutation) page covers that path.
8286

83-
Use an anchor when navigation is represented by a link in the interface.
84-
Use `useNavigate` for navigation caused by application logic.
87+
## Read the location
8588

86-
## Read location and parameters
87-
88-
`useLocation` returns the reactive location object:
89+
`useLocation()` returns a reactive object describing where the user is:
8990

9091
```tsx
9192
const location = useLocation();
9293

93-
location.pathname;
94-
location.search;
94+
location.pathname; // "/products/mug"
95+
location.search; // "?ref=home"
96+
location.query; // { ref: "home" }
9597
location.hash;
96-
location.query;
9798
location.state;
98-
location.key;
99+
location.key; // changes on every navigation
99100
```
100101

101-
`useParams` returns the merged parameters for the current route chain:
102+
Each field is reactive on its own.
103+
A memo that reads `location.pathname` does not re-run when only the hash changes.
102104

103-
```tsx
104-
const params = useParams();
105-
params.id;
106-
```
107-
108-
Pass a typed path node to narrow the known parameter keys:
105+
`useParams()` returns the merged parameters of the current match, and a typed path narrows the keys:
109106

110107
```tsx
111-
const params = useParams(Router.paths.users);
112-
params.id;
108+
const params = useParams(Router.paths.products);
109+
params.id; // string
113110
```
114111

112+
Inside a route component, `props.params` is the same object, already typed when the component is declared with `RouteProps`.
113+
115114
## Type search parameters
116115

117-
Without a schema, `useSearchParams()` reads raw string or string-array values.
118-
The `setSearchParams` function merges values into the current query string.
119-
By default, the update navigates without scrolling.
120-
Empty strings, `undefined`, and `null` remove a key.
116+
Search parameters are the right home for state that should survive a refresh and be shareable: a filter, a sort order, a page number.
117+
Read and write them with `useSearchParams`:
121118

122119
```tsx
123120
const [search, setSearch] = useSearchParams();
124121

125-
<button onClick={() => setSearch({ page: Number(search.page || 0) + 1 })}>
122+
<button onClick={() => setSearch({ page: Number(search.page || 1) + 1 })}>
126123
Next page
127124
</button>;
128125
```
129126

130-
Pass a path node whose route defines a synchronous Standard Schema validator to parse and type search values:
127+
`setSearch` merges into the current query string and navigates without scrolling.
128+
Setting a key to `""`, `undefined`, or `null` removes it.
129+
130+
Without a schema every value is a string or an array of strings, and `Number(search.page || 1)` is on you.
131+
Give the route a `search` schema, any synchronous Standard Schema validator, and pass the path node to get parsed, typed values:
132+
133+
```tsx
134+
// src/router.ts
135+
import * as v from "valibot";
136+
137+
{
138+
path: "/search",
139+
search: v.object({
140+
q: v.optional(v.string(), ""),
141+
page: v.optional(v.pipe(v.unknown(), v.transform(Number)), 1),
142+
}),
143+
component: Search,
144+
}
145+
```
146+
147+
The query string only holds strings, so the schema is where `"2"` becomes `2`; a plain `v.number()` would reject every value.
131148

132149
```tsx
150+
// src/pages/Search.tsx
133151
const [search, setSearch] = useSearchParams(Router.paths.search);
134152

135-
search.page;
153+
search.page; // number
136154
setSearch({ page: search.page + 1 });
137155
```
138156

139-
The router runs schemas from the current root-to-leaf match chain.
140-
Successful schema outputs merge over the raw query values.
141-
A schema result with issues is skipped.
142-
An asynchronous schema result throws because asynchronous search validation is not supported.
157+
The router runs the schemas of every route in the current match, root to leaf, and merges the parsed outputs over the raw values.
158+
A schema that reports issues is skipped for that read, so the raw strings remain rather than the page throwing.
159+
Asynchronous schemas are not supported and throw.
160+
161+
## Show active and pending links
143162

144-
## Style active and pending links
163+
Click a link to a page whose data takes a moment.
164+
The current page stays on screen, and the clicked link gets a `data-pending` attribute until the destination is ready.
165+
That is the visible half of the held update described in [Async reactivity](/concepts/async-reactivity#settled-view-and-in-flight-work), and it is why an app without any loading spinner still feels responsive: the link itself shows that something is happening.
145166

146-
Claimed route anchors receive state attributes:
167+
The router sets three attributes on the anchors it handles:
147168

148-
- `aria-current="page"` marks an exact match.
149-
- `data-active` marks an exact or descendant match.
150-
- `data-pending` marks the target of an in-flight navigation.
169+
- `aria-current="page"` on an exact match.
170+
- `data-active` on an exact or descendant match, so the Account link is active on `/account/orders/42`.
171+
- `data-pending` on the target of an in-flight navigation.
151172

152-
The root path matches exactly for active state.
173+
Style them in CSS with no component code:
153174

154175
```css
155176
nav a[aria-current="page"] {
@@ -162,13 +183,18 @@ nav a[data-active] {
162183

163184
a[data-pending] {
164185
opacity: 0.6;
186+
cursor: progress;
165187
}
166188
```
167189

168-
Use `useLinkState` when a custom link component needs reactive `active`, `current`, or `pending` accessors.
190+
The root path `/` is active only on an exact match; otherwise it would be active everywhere.
191+
192+
For a component that is not an anchor, or an anchor that needs the state in JSX, `useLinkState` returns the same three as accessors:
169193

170194
```tsx
171-
function TabLink(props: { href: string; children: JSX.Element }) {
195+
import { useLinkState } from "@solidjs/router";
196+
197+
function Tab(props: { href: string; children: JSX.Element }) {
172198
const state = useLinkState(() => props.href, { end: true });
173199

174200
return (
@@ -179,43 +205,52 @@ function TabLink(props: { href: string; children: JSX.Element }) {
179205
}
180206
```
181207

182-
Configure anchor navigation with these attributes:
183-
184-
- `replace` replaces the current history entry.
185-
- `noscroll` prevents scrolling after navigation.
186-
- `state` supplies JSON for `location.state`.
187-
- `preload` controls route-data preloading.
188-
189-
Set `preload="false"` to keep route code preloading while skipping that link's route-data preload.
190-
Set `link` when the router uses `explicitLinks: true`.
208+
`end: true` asks for exact matching, the `aria-current` rule rather than the `data-active` rule.
191209

192210
## Observe and guard navigation
193211

194-
`useIsRouting()` returns an accessor that is true while a programmatic or native navigation waits for its route work to settle.
195-
`useMatch()` tests a supplied pattern against the current pathname.
196-
`useRouteMatches()` returns the resolved route-definition match chain.
212+
`useIsRouting()` is true while a navigation is waiting on route work.
213+
Use it for a top-of-page progress bar; for a single link, `data-pending` is already there.
197214

198-
```tsx
199-
const section = useMatch(() => "/docs/*rest");
200-
const matches = useRouteMatches();
201-
const isRouting = useIsRouting();
202-
```
215+
`useMatch(() => pattern)` tests a pattern against the current pathname without needing a route for it, and `useRouteMatches()` returns the matched route definitions with their `info`, which is how a breadcrumb reads route metadata.
203216

204-
Use `useBeforeLeave` to prevent a route change and retry it after confirmation:
217+
To stop the user leaving a page with unsaved changes, register a leave guard:
205218

206219
```tsx
220+
import { useBeforeLeave } from "@solidjs/router";
221+
207222
useBeforeLeave((event) => {
208223
if (!dirty()) return;
209-
210224
event.preventDefault();
211-
212225
if (window.confirm("Discard unsaved changes?")) {
213226
event.retry(true);
214227
}
215228
});
216229
```
217230

218-
The guard applies to router navigation and supported browser history traversal.
219-
Pass `true` to `retry` to skip the leave handlers for the retried navigation.
231+
The guard runs for router navigations and for the browser history traversal the router can intercept.
232+
`retry(true)` re-issues the navigation and skips the guards, so the confirmation does not appear twice.
233+
It cannot stop the user closing the tab; pair it with a `beforeunload` listener if that matters.
234+
235+
## Common problems
236+
237+
**Clicking a link reloads the whole page.**
238+
The anchor is outside the `<Router>`, has a `target`, or points to a different origin.
239+
With `explicitLinks: true`, it is missing the `link` attribute.
240+
241+
**`paths.products` is a type error.**
242+
The route is `/products/:id`, so the node needs a call: `paths.products(id)`.
243+
The reverse is also true; a static route is `paths.account`, not `paths.account()`.
244+
245+
**The active style is on every link.**
246+
The style targets `data-active` on the `/` link, which is a parent of everything.
247+
Use `aria-current="page"` for the home link, or `useLinkState` with `end: true`.
248+
249+
**`search.page` is a string.**
250+
There is no `search` schema on the route, or the path node was not passed to `useSearchParams`.
251+
252+
## Next steps
220253

221-
See the [navigation API reference](/reference/solid-router/navigation) for all primitive signatures.
254+
- [Nested routes and layouts](/routing/solid-router/nested-routes): where section navigation lives and what stays mounted across links.
255+
- [Data loading and mutations](/routing/solid-router/data): what the router preloads on hover, and `redirect()` from an action.
256+
- [Navigation API reference](/reference/solid-router/navigation): signatures for every primitive on this page.

0 commit comments

Comments
 (0)