You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
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."
5
5
---
6
6
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).
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.
19
26
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.
25
28
26
-
The browser handles these anchors.
29
+
A few attributes adjust how the router treats one link:
27
30
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.
29
36
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`
32
38
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:
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.
46
51
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`.
49
54
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).
52
60
53
61
## Navigate from code
54
62
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`:
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.
78
84
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.
82
86
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
85
88
86
-
## Read location and parameters
87
-
88
-
`useLocation` returns the reactive location object:
89
+
`useLocation()` returns a reactive object describing where the user is:
89
90
90
91
```tsx
91
92
const location =useLocation();
92
93
93
-
location.pathname;
94
-
location.search;
94
+
location.pathname; // "/products/mug"
95
+
location.search; // "?ref=home"
96
+
location.query; // { ref: "home" }
95
97
location.hash;
96
-
location.query;
97
98
location.state;
98
-
location.key;
99
+
location.key;// changes on every navigation
99
100
```
100
101
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.
102
104
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:
109
106
110
107
```tsx
111
-
const params =useParams(Router.paths.users);
112
-
params.id;
108
+
const params =useParams(Router.paths.products);
109
+
params.id;// string
113
110
```
114
111
112
+
Inside a route component, `props.params` is the same object, already typed when the component is declared with `RouteProps`.
113
+
115
114
## Type search parameters
116
115
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.
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
143
162
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.
145
166
146
-
Claimed route anchors receive state attributes:
167
+
The router sets three attributes on the anchors it handles:
147
168
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.
151
172
152
-
The root path matches exactly for active state.
173
+
Style them in CSS with no component code:
153
174
154
175
```css
155
176
nava[aria-current="page"] {
@@ -162,13 +183,18 @@ nav a[data-active] {
162
183
163
184
a[data-pending] {
164
185
opacity: 0.6;
186
+
cursor: progress;
165
187
}
166
188
```
167
189
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:
169
193
170
194
```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 }) {
172
198
const state =useLinkState(() =>props.href, { end: true });
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.
191
209
192
210
## Observe and guard navigation
193
211
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.
197
214
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.
203
216
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:
205
218
206
219
```tsx
220
+
import { useBeforeLeave } from"@solidjs/router";
221
+
207
222
useBeforeLeave((event) => {
208
223
if (!dirty()) return;
209
-
210
224
event.preventDefault();
211
-
212
225
if (window.confirm("Discard unsaved changes?")) {
213
226
event.retry(true);
214
227
}
215
228
});
216
229
```
217
230
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
220
253
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