@solidjs/router 1.0.0 → 2.0.0-next.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +606 -697
- package/dist/claims.d.ts +21 -0
- package/dist/claims.js +115 -0
- package/dist/data/action.d.ts +40 -11
- package/dist/data/action.js +307 -101
- package/dist/data/events.d.ts +8 -0
- package/dist/data/events.js +24 -23
- package/dist/data/index.d.ts +2 -4
- package/dist/data/index.js +2 -4
- package/dist/data/query.d.ts +1 -3
- package/dist/data/query.js +75 -32
- package/dist/data/serverForms.d.ts +1 -0
- package/dist/data/serverForms.js +5 -0
- package/dist/fs.d.ts +94 -0
- package/dist/fs.js +51 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +1949 -1204
- package/dist/index.jsx +2 -2
- package/dist/lifecycle.d.ts +29 -4
- package/dist/lifecycle.js +40 -37
- package/dist/paths.d.ts +117 -0
- package/dist/paths.js +41 -0
- package/dist/routers/components.d.ts +12 -26
- package/dist/routers/components.jsx +68 -54
- package/dist/routers/factory.d.ts +121 -0
- package/dist/routers/factory.jsx +154 -0
- package/dist/routers/history.d.ts +26 -0
- package/dist/routers/history.js +180 -0
- package/dist/routers/index.d.ts +5 -11
- package/dist/routers/index.js +2 -6
- package/dist/routers/scrollRestoration.d.ts +10 -1
- package/dist/routers/scrollRestoration.js +49 -11
- package/dist/routing.d.ts +96 -52
- package/dist/routing.js +429 -172
- package/dist/server.d.ts +47 -0
- package/dist/server.js +156 -0
- package/dist/types.d.ts +158 -44
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +2 -0
- package/package.json +12 -7
- package/dist/components.d.ts +0 -31
- package/dist/components.jsx +0 -40
- package/dist/data/createAsync.d.ts +0 -32
- package/dist/data/createAsync.js +0 -96
- package/dist/data/response.d.ts +0 -4
- package/dist/data/response.js +0 -42
- package/dist/routers/HashRouter.d.ts +0 -9
- package/dist/routers/HashRouter.js +0 -41
- package/dist/routers/MemoryRouter.d.ts +0 -24
- package/dist/routers/MemoryRouter.js +0 -57
- package/dist/routers/Router.d.ts +0 -17
- package/dist/routers/Router.js +0 -59
- package/dist/routers/StaticRouter.d.ts +0 -6
- package/dist/routers/StaticRouter.js +0 -15
- package/dist/routers/createRouter.d.ts +0 -10
- package/dist/routers/createRouter.js +0 -41
package/README.md
CHANGED
|
@@ -10,947 +10,775 @@
|
|
|
10
10
|
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
**Solid Router** brings fine-grained reactivity to route navigation
|
|
13
|
+
**Solid Router** brings fine-grained reactivity to route navigation. Routes are config objects — the single source of truth for matching *and* types — and the router upgrades HTML's own interaction verbs instead of wrapping them: `<a href={path}>` and `<form action={action}>` carry typed, URL-addressable values on real platform elements, intercepted by delegation, decorated with a shared attribute vocabulary, and fully functional without JavaScript.
|
|
14
14
|
|
|
15
15
|
Explore the official [documentation](https://docs.solidjs.com/solid-router) for detailed guides and examples.
|
|
16
16
|
|
|
17
17
|
## Core Features
|
|
18
18
|
|
|
19
|
-
- **
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- **
|
|
25
|
-
- **Universal Rendering**: Seamless rendering on both client and server environments
|
|
26
|
-
- **Declarative**: Define routes as components or as an object
|
|
27
|
-
- **Preload Functions**: Parallel data fetching, following the render-as-you-fetch pattern
|
|
28
|
-
- **Dynamic Route Parameters**: Flexible URL patterns with parameters, optional segments, and wildcards
|
|
29
|
-
- **Data APIs with Caching**: Reactive data fetching with deduplication and revalidation
|
|
19
|
+
- **Typed Routing**: URLs built through a typed path proxy inferred from your route config — `paths.users(2).settings` typechecks against the tree
|
|
20
|
+
- **Plain Anchors**: no link component — `<a>` elements get `aria-current`, `data-active`, and `data-pending` automatically via compiler-claimed anchors
|
|
21
|
+
- **Universal Rendering**: one factory for browser, hash, memory, and server rendering; history adapters are imports, so unused ones never enter your bundle
|
|
22
|
+
- **Preload Functions**: parallel data fetching following the render-as-you-fetch pattern, triggered eagerly on link hover/focus
|
|
23
|
+
- **Data APIs with Caching**: `query` and `action` with deduplication, revalidation, single-flight mutations, and progressive enhancement
|
|
24
|
+
- **Typed Search Params**: opt-in per-route [Standard Schema](https://github.com/standard-schema/standard-schema) validation — `search.page` is a `number`, not `"2"`
|
|
30
25
|
|
|
31
|
-
## Table of
|
|
26
|
+
## Table of Contents
|
|
32
27
|
|
|
33
28
|
- [Getting Started](#getting-started)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- [
|
|
37
|
-
- [
|
|
38
|
-
- [
|
|
39
|
-
- [
|
|
40
|
-
- [
|
|
29
|
+
- [The Mental Model: Instance vs Hooks](#the-mental-model-instance-vs-hooks)
|
|
30
|
+
- [Route Definitions](#route-definitions)
|
|
31
|
+
- [Dynamic Routes](#dynamic-routes)
|
|
32
|
+
- [Typed Route Params](#typed-route-params)
|
|
33
|
+
- [Match Filters](#match-filters)
|
|
34
|
+
- [Optional Parameters](#optional-parameters)
|
|
35
|
+
- [Wildcard Routes](#wildcard-routes)
|
|
36
|
+
- [Multiple Paths](#multiple-paths)
|
|
37
|
+
- [Nested Routes](#nested-routes)
|
|
38
|
+
- [Lazy Route Subtrees](#lazy-route-subtrees)
|
|
39
|
+
- [File-System Routes](#file-system-routes)
|
|
40
|
+
- [Typed Paths](#typed-paths)
|
|
41
|
+
- [Links](#links)
|
|
42
|
+
- [Preload Functions](#preload-functions)
|
|
41
43
|
- [Data APIs](#data-apis)
|
|
42
|
-
- [
|
|
43
|
-
- [
|
|
44
|
+
- [Typed Search Params](#typed-search-params)
|
|
45
|
+
- [Router Config Reference](#router-config-reference)
|
|
44
46
|
- [Router Primitives](#router-primitives)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- [useSearchParams](#usesearchparams)
|
|
49
|
-
- [useIsRouting](#useisrouting)
|
|
50
|
-
- [useMatch](#usematch)
|
|
51
|
-
- [useCurrentMatches](#useCurrentMatches)
|
|
52
|
-
- [useBeforeLeave](#usebeforeleave)
|
|
47
|
+
- [Other Environments](#other-environments)
|
|
48
|
+
- [Server Integration](#server-integration)
|
|
49
|
+
- [Migration from 0.x](#migration-from-0x)
|
|
53
50
|
- [SPAs in Deployed Environments](#spas-in-deployed-environments)
|
|
54
51
|
|
|
55
52
|
## Getting Started
|
|
56
53
|
|
|
57
|
-
### Set Up the Router
|
|
58
|
-
|
|
59
54
|
```bash
|
|
60
55
|
# use preferred package manager
|
|
61
56
|
npm add @solidjs/router
|
|
62
57
|
```
|
|
63
58
|
|
|
64
|
-
|
|
59
|
+
Define your routes as config objects and create the router outside JSX. The instance is the provider component, and `paths` is a typed URL builder inferred from the tree:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// app/router.ts
|
|
63
|
+
import { lazy } from "solid-js";
|
|
64
|
+
import { createRouter } from "@solidjs/router";
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
export const Router = createRouter({
|
|
67
|
+
routes: [
|
|
68
|
+
{ path: "/", component: lazy(() => import("./pages/Home")) },
|
|
69
|
+
{ path: "/about", component: lazy(() => import("./pages/About")) },
|
|
70
|
+
{
|
|
71
|
+
path: "/users/:id",
|
|
72
|
+
component: lazy(() => import("./pages/User")),
|
|
73
|
+
children: [
|
|
74
|
+
{ path: "/", component: lazy(() => import("./pages/UserOverview")) },
|
|
75
|
+
{ path: "/settings", component: lazy(() => import("./pages/UserSettings")) }
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{ path: "*404", component: lazy(() => import("./pages/NotFound")) }
|
|
79
|
+
]
|
|
80
|
+
});
|
|
69
81
|
|
|
70
|
-
|
|
82
|
+
export const { paths } = Router;
|
|
71
83
|
```
|
|
72
84
|
|
|
73
|
-
This
|
|
85
|
+
This one module serves everything: the client renders the instance, and on the server the same instance reads its location from the request (or the configured history when there is none — SSG, tests). There is no separate "server router" and no need to export the raw route array.
|
|
74
86
|
|
|
75
|
-
|
|
87
|
+
When a tree is composed across files (feature subtrees), wrap the extracted arrays in `defineRoutes` — an identity function that preserves the literal types inference feeds on (a bare extracted array silently widens to `string` paths without it or `as const`) and type-checks the definitions where they're written:
|
|
76
88
|
|
|
77
|
-
|
|
89
|
+
```tsx
|
|
90
|
+
// features/admin/routes.ts
|
|
91
|
+
export const adminRoutes = defineRoutes([
|
|
92
|
+
{ path: "/admin", component: Admin, children: [/* ... */] }
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
// app/router.ts
|
|
96
|
+
export const Router = createRouter({ routes: [...appRoutes, ...adminRoutes] });
|
|
97
|
+
```
|
|
78
98
|
|
|
79
|
-
|
|
99
|
+
Its single-route sibling `defineRoute` also types `params` inside the route's own `component` and `preload` — see [Typed Route Params](#typed-route-params).
|
|
80
100
|
|
|
81
|
-
|
|
82
|
-
import { render } from "solid-js/web";
|
|
83
|
-
import { Router, Route } from "@solidjs/router";
|
|
101
|
+
Mount it by rendering the instance. The render-prop child is your root layout — it always stays mounted, receives the matched content as `props.children`, and is the ideal place for top-level navigation and context providers:
|
|
84
102
|
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
```tsx
|
|
104
|
+
// app/index.tsx
|
|
105
|
+
import { render } from "@solidjs/web";
|
|
106
|
+
import { Router } from "./router";
|
|
87
107
|
|
|
88
108
|
render(
|
|
89
109
|
() => (
|
|
90
110
|
<Router>
|
|
91
|
-
|
|
92
|
-
|
|
111
|
+
{props => (
|
|
112
|
+
<>
|
|
113
|
+
<h1>My Site with lots of pages</h1>
|
|
114
|
+
{props.children}
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
93
117
|
</Router>
|
|
94
118
|
),
|
|
95
|
-
document.getElementById("app")
|
|
119
|
+
document.getElementById("app")!
|
|
96
120
|
);
|
|
97
121
|
```
|
|
98
122
|
|
|
99
|
-
|
|
123
|
+
Links are plain anchors. Typed path nodes coerce to strings on the attribute, and the router intercepts clicks through delegation:
|
|
100
124
|
|
|
101
|
-
|
|
125
|
+
```tsx
|
|
126
|
+
import { paths } from "./router";
|
|
102
127
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
128
|
+
<nav>
|
|
129
|
+
<a href={paths()}>Home</a>
|
|
130
|
+
<a href={paths.about}>About</a>
|
|
131
|
+
<a href={paths.users(user.id).settings}>Settings</a>
|
|
132
|
+
</nav>;
|
|
133
|
+
```
|
|
106
134
|
|
|
107
|
-
|
|
108
|
-
import Users from "./pages/Users";
|
|
135
|
+
## The Mental Model: Instance vs Hooks
|
|
109
136
|
|
|
110
|
-
|
|
111
|
-
<>
|
|
112
|
-
<h1>My Site with lots of pages</h1>
|
|
113
|
-
{props.children}
|
|
114
|
-
</>
|
|
115
|
-
);
|
|
137
|
+
The API splits across two surfaces, and the line between them is precise: **could two concurrent server requests give different answers? If yes, it's a hook. If no, it's on the instance.**
|
|
116
138
|
|
|
117
|
-
|
|
118
|
-
() => (
|
|
119
|
-
<Router root={App}>
|
|
120
|
-
<Route path="/users" component={Users} />
|
|
121
|
-
<Route path="/" component={Home} />
|
|
122
|
-
</Router>
|
|
123
|
-
),
|
|
124
|
-
document.getElementById("app")
|
|
125
|
-
);
|
|
126
|
-
```
|
|
139
|
+
The instance is shared — one module-level object serving every mount, every request, every test. It is deliberately non-stateful (on the server there are many "current locations" at once), so it carries only the app's static routing vocabulary. Hooks read the live session from context.
|
|
127
140
|
|
|
128
|
-
|
|
141
|
+
| Instance — facts about the *app* | Hooks — facts about the *session* |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `paths` — how to spell URLs | `useLocation`, `useParams` — where am I |
|
|
144
|
+
| `match(url)` — how would a URL match | `useNavigate`, `usePreloadRoute` — move / warm |
|
|
145
|
+
| `routes`, `config` — what exists | `useIsRouting`, `useRouteMatches`, `useSearchParams` — live state |
|
|
129
146
|
|
|
130
|
-
|
|
147
|
+
They compose as noun and verb — the instance supplies a typed URL, the hook acts on the current session:
|
|
131
148
|
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
|
|
149
|
+
```tsx
|
|
150
|
+
const navigate = useNavigate();
|
|
151
|
+
navigate(paths.users(2)); // verb(noun)
|
|
135
152
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
import NotFound from "./pages/404";
|
|
153
|
+
const params = useParams(paths.users); // hook, typed by the instance
|
|
154
|
+
```
|
|
139
155
|
|
|
140
|
-
|
|
141
|
-
<>
|
|
142
|
-
<h1>My Site with lots of pages</h1>
|
|
143
|
-
{props.children}
|
|
144
|
-
</>
|
|
145
|
-
);
|
|
156
|
+
**Hooks are the default; import the router only when you need typed URLs or matching outside a render.** Components that only read their session (params, location, string-path navigation) never need the instance — which also means component files don't form import cycles with the router module that references them in its config.
|
|
146
157
|
|
|
147
|
-
|
|
148
|
-
() => (
|
|
149
|
-
<Router root={App}>
|
|
150
|
-
<Route path="/users" component={Users} />
|
|
151
|
-
<Route path="/" component={Home} />
|
|
152
|
-
<Route path="*404" component={NotFound} />
|
|
153
|
-
</Router>
|
|
154
|
-
),
|
|
155
|
-
document.getElementById("app")
|
|
156
|
-
);
|
|
157
|
-
```
|
|
158
|
+
## Route Definitions
|
|
158
159
|
|
|
159
|
-
|
|
160
|
+
A route definition supports:
|
|
160
161
|
|
|
161
|
-
|
|
162
|
+
| key | type | description |
|
|
163
|
+
| -------------- | --------------------------------------- | ------------------------------------------------------------------ |
|
|
164
|
+
| `path` | `string \| string[]` | Path partial for this route segment |
|
|
165
|
+
| `component` | `Component` | Component rendered for the matched segment |
|
|
166
|
+
| `children` | `RouteDefinition \| RouteDefinition[] \| () => Promise<...>` | Nested route definitions, or a thunk for a [lazy subtree](#lazy-route-subtrees) |
|
|
167
|
+
| `preload` | `RoutePreloadFunc` | Called on preload intent (hover/focus) and navigation |
|
|
168
|
+
| `matchFilters` | `MatchFilters` | Additional constraints for matching parameters |
|
|
169
|
+
| `search` | `StandardSchemaV1` | Search-param validator; its types flow into `paths` and hooks |
|
|
170
|
+
| `info` | `Record<string, any>` | Arbitrary metadata, readable via `useRouteMatches` |
|
|
162
171
|
|
|
163
|
-
|
|
164
|
-
import { lazy } from "solid-js";
|
|
165
|
-
import { render } from "solid-js/web";
|
|
166
|
-
import { Router, Route } from "@solidjs/router";
|
|
172
|
+
The tree is **immutable and there is one router per app** — that's what makes `paths` and the typed hooks truthful, it lets matching compile once and be shared by every mount, request, and `match()` call, and it means delegation, link state, and preloading all have a single owner. Compose large apps by spreading subtrees into the config (see `defineRoutes` above); mounting a router inside another router is not supported (nested `<Routes>` has been gone since 0.10) and warns in development. Sections whose *code* shouldn't load up front are [lazy route subtrees](#lazy-route-subtrees) — still one tree, still typed.
|
|
167
173
|
|
|
168
|
-
|
|
169
|
-
const Home = lazy(() => import("./pages/Home"));
|
|
174
|
+
### Dynamic Routes
|
|
170
175
|
|
|
171
|
-
|
|
172
|
-
<>
|
|
173
|
-
<h1>My Site with lots of pages</h1>
|
|
174
|
-
{props.children}
|
|
175
|
-
</>
|
|
176
|
-
);
|
|
176
|
+
Treat part of the path as a parameter with a colon:
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
</Router>
|
|
184
|
-
),
|
|
185
|
-
document.getElementById("app")
|
|
186
|
-
);
|
|
178
|
+
```tsx
|
|
179
|
+
const routes = defineRoutes([
|
|
180
|
+
{ path: "/users", component: Users },
|
|
181
|
+
{ path: "/users/:id", component: User }
|
|
182
|
+
]);
|
|
187
183
|
```
|
|
188
184
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
Use an anchor tag that takes you to a route:
|
|
185
|
+
As long as the URL fits the pattern, the `User` component shows, and `id` is available via `useParams`.
|
|
192
186
|
|
|
193
|
-
|
|
194
|
-
import { lazy } from "solid-js";
|
|
195
|
-
import { render } from "solid-js/web";
|
|
196
|
-
import { Router, Route } from "@solidjs/router";
|
|
197
|
-
|
|
198
|
-
const Users = lazy(() => import("./pages/Users"));
|
|
199
|
-
const Home = lazy(() => import("./pages/Home"));
|
|
200
|
-
|
|
201
|
-
const App = (props) => (
|
|
202
|
-
<>
|
|
203
|
-
<nav>
|
|
204
|
-
<a href="/about">About</a>
|
|
205
|
-
<a href="/">Home</a>
|
|
206
|
-
</nav>
|
|
207
|
-
<h1>My Site with lots of pages</h1>
|
|
208
|
-
{props.children}
|
|
209
|
-
</>
|
|
210
|
-
);
|
|
187
|
+
**Note on Animation/Transitions**: routes that share the same path match are treated as the same route. To force a re-render, wrap your component in a keyed `<Show>`:
|
|
211
188
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
<Route path="/" component={Home} />
|
|
217
|
-
</Router>
|
|
218
|
-
),
|
|
219
|
-
document.getElementById("app")
|
|
220
|
-
);
|
|
189
|
+
```tsx
|
|
190
|
+
<Show when={params.something} keyed>
|
|
191
|
+
<MyComponent />
|
|
192
|
+
</Show>
|
|
221
193
|
```
|
|
222
194
|
|
|
223
|
-
|
|
195
|
+
### Typed Route Params
|
|
224
196
|
|
|
225
|
-
|
|
197
|
+
By default `params` is an open record — every key is `string | undefined`, even when the pattern guarantees it. Wrap a route in `defineRoute` and its `component` and `preload` are typed from the route's own `path`:
|
|
226
198
|
|
|
227
|
-
```
|
|
228
|
-
import {
|
|
229
|
-
import { render } from "solid-js/web";
|
|
230
|
-
import { Router, Route } from "@solidjs/router";
|
|
231
|
-
|
|
232
|
-
const Users = lazy(() => import("./pages/Users"));
|
|
233
|
-
const User = lazy(() => import("./pages/User"));
|
|
234
|
-
const Home = lazy(() => import("./pages/Home"));
|
|
199
|
+
```tsx
|
|
200
|
+
import { defineRoute } from "@solidjs/router";
|
|
235
201
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
);
|
|
202
|
+
const story = defineRoute({
|
|
203
|
+
path: "/stories/:id/:tab?",
|
|
204
|
+
preload: ({ params }) => getStory(params.id), // params.id: string
|
|
205
|
+
component: props => (
|
|
206
|
+
<Story
|
|
207
|
+
id={props.params.id} // string — the pattern guarantees it
|
|
208
|
+
tab={props.params.tab} // string | undefined — optional param
|
|
209
|
+
/>
|
|
210
|
+
)
|
|
211
|
+
});
|
|
246
212
|
```
|
|
247
213
|
|
|
248
|
-
|
|
214
|
+
`defineRoute` is an identity function at runtime — the route object drops into `routes` (or a parent's `children`) like any plain object, and `path`, `children`, `matchFilters`, and `search` still flow into `paths` and the typed hooks. Params inherited from parent routes stay accessible as `string | undefined`; nested `children` type their own params only if they use `defineRoute` themselves.
|
|
249
215
|
|
|
250
|
-
|
|
216
|
+
For components declared away from their route, `RouteProps` takes a path witness — the same `paths` node you navigate with (`import type` keeps the instance out of the runtime graph, so no cycle) — plus an optional data type:
|
|
251
217
|
|
|
252
|
-
|
|
253
|
-
|
|
218
|
+
```tsx
|
|
219
|
+
import type { RouteComponent, RouteProps } from "@solidjs/router";
|
|
220
|
+
import type { Router } from "./app/router";
|
|
254
221
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
222
|
+
function Story(props: RouteProps<typeof Router.paths.stories, StoryData>) {
|
|
223
|
+
props.params.id; // string
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// component-type form — props infer contextually
|
|
227
|
+
const Story: RouteComponent<typeof Router.paths.stories, StoryData> = props => (
|
|
228
|
+
<h1>{props.params.id}</h1>
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// or, anywhere under the route:
|
|
232
|
+
const params = useParams(paths.stories); // typed from the tree
|
|
259
233
|
```
|
|
260
234
|
|
|
261
|
-
|
|
235
|
+
When no instance is in scope at the definition site — most notably [file-system route files](#file-system-routes), where the pattern lives in the filename — the witness can be the pattern string itself: `RouteProps<"/stories/:id">`.
|
|
262
236
|
|
|
263
|
-
|
|
264
|
-
This allows for more complex routing descriptions than just checking the presence of a parameter.
|
|
237
|
+
### Match Filters
|
|
265
238
|
|
|
266
|
-
|
|
267
|
-
import { lazy } from "solid-js";
|
|
268
|
-
import { render } from "solid-js/web";
|
|
269
|
-
import { Router, Route } from "@solidjs/router";
|
|
270
|
-
import type { MatchFilters } from "@solidjs/router";
|
|
239
|
+
Each parameter can be validated with a `MatchFilter` — an enum array, a regex, or a predicate. If validation fails, the route doesn't match:
|
|
271
240
|
|
|
272
|
-
|
|
241
|
+
```tsx
|
|
242
|
+
import { int, type MatchFilters } from "@solidjs/router";
|
|
273
243
|
|
|
274
244
|
const filters: MatchFilters = {
|
|
275
|
-
parent: ["mom", "dad"],
|
|
276
|
-
id: /^\d+$/,
|
|
277
|
-
withHtmlExtension: (v: string) => v.length > 5 && v.endsWith(".html")
|
|
245
|
+
parent: ["mom", "dad"], // enum values
|
|
246
|
+
id: /^\d+$/, // only numbers
|
|
247
|
+
withHtmlExtension: (v: string) => v.length > 5 && v.endsWith(".html")
|
|
278
248
|
};
|
|
279
249
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
<Route
|
|
284
|
-
path="/users/:parent/:id/:withHtmlExtension"
|
|
285
|
-
component={User}
|
|
286
|
-
matchFilters={filters}
|
|
287
|
-
/>
|
|
288
|
-
</Router>
|
|
289
|
-
),
|
|
290
|
-
document.getElementById("app")
|
|
291
|
-
);
|
|
250
|
+
const routes = defineRoutes([
|
|
251
|
+
{ path: "/users/:parent/:id/:withHtmlExtension", component: User, matchFilters: filters }
|
|
252
|
+
]);
|
|
292
253
|
```
|
|
293
254
|
|
|
294
|
-
|
|
295
|
-
If the validation fails, the route will not match.
|
|
255
|
+
So `/users/mom/123/contact.html` matches, while `/users/aunt/123/contact.html` (invalid `parent`) and `/users/mom/me/contact.html` (non-numeric `id`) don't.
|
|
296
256
|
|
|
297
|
-
|
|
257
|
+
The built-in `int` filter is *typed*: it constrains matching to integers at runtime and types the param as `number` at `paths` callsites:
|
|
298
258
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
- `/users/aunt/123/contact.html` would not match as `:parent` is not 'mom' or 'dad',
|
|
302
|
-
- `/users/mom/me/contact.html` would not match as `:id` is not a number,
|
|
303
|
-
- `/users/dad/123/contact` would not match as `:withHtmlExtension` is missing `.html`.
|
|
259
|
+
```tsx
|
|
260
|
+
{ path: "/users/:id", matchFilters: { id: int }, component: User }
|
|
304
261
|
|
|
305
|
-
|
|
262
|
+
paths.users(123); // ok
|
|
263
|
+
paths.users("abc"); // type error
|
|
264
|
+
```
|
|
306
265
|
|
|
307
266
|
### Optional Parameters
|
|
308
267
|
|
|
309
|
-
|
|
268
|
+
Add a question mark to make a parameter optional:
|
|
310
269
|
|
|
311
|
-
```
|
|
270
|
+
```tsx
|
|
312
271
|
// Matches stories and stories/123 but not stories/123/comments
|
|
313
|
-
|
|
272
|
+
{ path: "/stories/:id?", component: Stories }
|
|
314
273
|
```
|
|
315
274
|
|
|
316
275
|
### Wildcard Routes
|
|
317
276
|
|
|
318
|
-
|
|
277
|
+
Use `*` to match any remainder of the path, optionally naming it to expose it as a parameter:
|
|
319
278
|
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
|
|
279
|
+
```tsx
|
|
280
|
+
{ path: "foo/*", component: Foo } // matches foo/, foo/a, foo/a/b/c
|
|
281
|
+
{ path: "foo/*any", component: Foo } // rest of the path available as params.any
|
|
323
282
|
```
|
|
324
283
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
```jsx
|
|
328
|
-
<Route path="foo/*any" component={Foo} />
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
Note that the wildcard token must be the last part of the path; `foo/*any/bar` won't create any routes.
|
|
284
|
+
The wildcard token must be the last part of the path; `foo/*any/bar` won't create any routes.
|
|
332
285
|
|
|
333
286
|
### Multiple Paths
|
|
334
287
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
```jsx
|
|
338
|
-
// Navigating from login to register does not cause the Login component to re-render
|
|
339
|
-
<Route path={["login", "register"]} component={Login} />
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
## Nested Routes
|
|
343
|
-
|
|
344
|
-
The following two route definitions have the same result:
|
|
345
|
-
|
|
346
|
-
```jsx
|
|
347
|
-
<Route path="/users/:id" component={User} />
|
|
348
|
-
```
|
|
288
|
+
An array of paths lets a route stay mounted (no re-render) when switching between locations it matches:
|
|
349
289
|
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
</Route>
|
|
290
|
+
```tsx
|
|
291
|
+
// Navigating from login to register does not re-render Login
|
|
292
|
+
{ path: ["login", "register"], component: Login }
|
|
354
293
|
```
|
|
355
294
|
|
|
356
|
-
|
|
295
|
+
### Nested Routes
|
|
357
296
|
|
|
358
|
-
Only leaf
|
|
297
|
+
Only leaf nodes become routes. A parent with a `component` wraps its children, which render where the parent places `props.children`:
|
|
359
298
|
|
|
360
|
-
```
|
|
361
|
-
//This won't work the way you'd expect
|
|
362
|
-
<Route path="/users" component={Users}>
|
|
363
|
-
<Route path="/:id" component={User} />
|
|
364
|
-
</Route>
|
|
365
|
-
|
|
366
|
-
// This works
|
|
367
|
-
<Route path="/users" component={Users} />
|
|
368
|
-
<Route path="/users/:id" component={User} />
|
|
369
|
-
|
|
370
|
-
// This also works
|
|
371
|
-
<Route path="/users">
|
|
372
|
-
<Route path="/" component={Users} />
|
|
373
|
-
<Route path="/:id" component={User} />
|
|
374
|
-
</Route>
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
You can also take advantage of nesting by using `props.children` passed to the route component.
|
|
378
|
-
|
|
379
|
-
```jsx
|
|
299
|
+
```tsx
|
|
380
300
|
function PageWrapper(props) {
|
|
381
301
|
return (
|
|
382
302
|
<div>
|
|
383
|
-
<h1>
|
|
303
|
+
<h1>We love our users!</h1>
|
|
384
304
|
{props.children}
|
|
385
|
-
<
|
|
305
|
+
<a href={paths()}>Back Home</a>
|
|
386
306
|
</div>
|
|
387
307
|
);
|
|
388
308
|
}
|
|
389
309
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
```jsx
|
|
401
|
-
<Route
|
|
402
|
-
path="/"
|
|
403
|
-
component={(props) => <div>Onion starts here {props.children}</div>}
|
|
404
|
-
>
|
|
405
|
-
<Route
|
|
406
|
-
path="layer1"
|
|
407
|
-
component={(props) => <div>Another layer {props.children}</div>}
|
|
408
|
-
>
|
|
409
|
-
<Route path="layer2" component={() => <div>Innermost layer</div>} />
|
|
410
|
-
</Route>
|
|
411
|
-
</Route>
|
|
310
|
+
const routes = defineRoutes([
|
|
311
|
+
{
|
|
312
|
+
path: "/users",
|
|
313
|
+
component: PageWrapper,
|
|
314
|
+
children: [
|
|
315
|
+
{ path: "/", component: Users },
|
|
316
|
+
{ path: "/:id", component: User }
|
|
317
|
+
]
|
|
318
|
+
}
|
|
319
|
+
]);
|
|
412
320
|
```
|
|
413
321
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
Even with smart caches it is possible that we have waterfalls both with view logic and with lazy loaded code. With preload functions, we can instead start fetching the data parallel to loading the route, so we can use the data as soon as possible. The preload function is called when the Route is loaded or eagerly when links are hovered.
|
|
417
|
-
|
|
418
|
-
As its only argument, the preload function is passed an object that you can use to access route information:
|
|
419
|
-
|
|
420
|
-
```js
|
|
421
|
-
import { lazy } from "solid-js";
|
|
422
|
-
import { Route } from "@solidjs/router";
|
|
423
|
-
|
|
424
|
-
const User = lazy(() => import("./pages/users/[id].js"));
|
|
322
|
+
You can nest indefinitely. In this example the only route created is `/layer1/layer2`, rendered as three nested divs:
|
|
425
323
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
324
|
+
```tsx
|
|
325
|
+
{
|
|
326
|
+
path: "/",
|
|
327
|
+
component: props => <div>Onion starts here {props.children}</div>,
|
|
328
|
+
children: [{
|
|
329
|
+
path: "layer1",
|
|
330
|
+
component: props => <div>Another layer {props.children}</div>,
|
|
331
|
+
children: [{ path: "layer2", component: () => <div>Innermost layer</div> }]
|
|
332
|
+
}]
|
|
429
333
|
}
|
|
430
|
-
|
|
431
|
-
// Pass it in the route definition
|
|
432
|
-
<Route path="/users/:id" component={User} preload={preloadUser} />;
|
|
433
334
|
```
|
|
434
335
|
|
|
435
|
-
|
|
436
|
-
| -------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
437
|
-
| params | object | The route parameters (same value as calling `useParams()` inside the route component) |
|
|
438
|
-
| location | `{ pathname, search, hash, query, state, key}` | An object that you can use to get more information about the path (corresponds to [`useLocation()`](#uselocation)) |
|
|
439
|
-
| intent | `"initial", "navigate", "native", "preload"` | Indicates why this function is being called. <ul><li>"initial" - the route is being initially shown (ie page load)</li><li>"native" - navigate originated from the browser (eg back/forward)</li><li>"navigate" - navigate originated from the router (eg call to navigate or anchor clicked)</li><li>"preload" - not navigating, just preloading (eg link hover)</li></ul> |
|
|
336
|
+
### Lazy Route Subtrees
|
|
440
337
|
|
|
441
|
-
|
|
338
|
+
`children` also accepts a thunk, so a whole section's route table (not just its components) stays out of the initial bundle:
|
|
442
339
|
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
340
|
+
```tsx
|
|
341
|
+
// admin/routes.ts
|
|
342
|
+
export default defineRoutes([
|
|
343
|
+
{ path: "/", component: lazy(() => import("./Dashboard")) },
|
|
344
|
+
{ path: "/users/:id", matchFilters: { id: int }, component: lazy(() => import("./User")) }
|
|
345
|
+
]);
|
|
448
346
|
|
|
449
|
-
//
|
|
450
|
-
|
|
347
|
+
// app.ts
|
|
348
|
+
const router = createRouter({
|
|
349
|
+
routes: [
|
|
350
|
+
{ path: "/", component: Home },
|
|
351
|
+
{ path: "/admin", component: AdminShell, children: () => import("./admin/routes") }
|
|
352
|
+
]
|
|
353
|
+
});
|
|
451
354
|
```
|
|
452
355
|
|
|
453
|
-
The
|
|
356
|
+
The import only fires when something needs the subtree — hovering a link into it, navigating into it, or the server matching a URL beneath it. Until then the tree carries a placeholder that knows every URL under `/admin` belongs to the subtree without knowing its contents (static sibling routes still win without triggering the load). Everything folds in as if the routes were inline:
|
|
454
357
|
|
|
455
|
-
|
|
358
|
+
- **Types**: TypeScript never runs the thunk — inference flows through the import's promise type, so `paths.admin.users(2)` typechecks (match filters and search schemas included) before any of the subtree's code exists client-side. The module's `default` or `routes` export is used. Only tables genuinely built at runtime (typed as plain `RouteDefinition[]`) degrade to untyped.
|
|
359
|
+
- **Navigation**: the table load folds into the navigation transition — the old screen holds until the subtree (and its matched components) are ready, exactly like a `lazy()` route component.
|
|
360
|
+
- **Preloading**: hover intent kicks the table load, and when it lands the preload continues into the inner routes' components and `preload` functions — one cascading warm-up from the earliest possible moment.
|
|
361
|
+
- **Server**: SSR resolves matched boundaries during the render (use the streaming entry points — `renderToStream`/`renderToStringAsync` — as with any async work), and the single-flight collector resolves them before its data pass.
|
|
456
362
|
|
|
457
|
-
|
|
363
|
+
Resolution is cached per thunk and append-only: the tree never changes shape after a subtree lands, it just gets more specific. Keep thunks deterministic — `() => import(...)` — rather than switching tables on runtime state.
|
|
458
364
|
|
|
459
|
-
###
|
|
460
|
-
|
|
461
|
-
To prevent duplicate fetching and to handle refetching triggers, we provide a query API that accepts a function and returns the same function.
|
|
462
|
-
|
|
463
|
-
```jsx
|
|
464
|
-
const getUser = query(async (id) => {
|
|
465
|
-
return (await fetch(`/api/users/${id}`)).json();
|
|
466
|
-
}, "users"); // used as the query key + serialized arguments
|
|
467
|
-
```
|
|
365
|
+
### File-System Routes
|
|
468
366
|
|
|
469
|
-
|
|
367
|
+
The `@solidjs/router/fs` adapter turns a `file-routes` manifest into route definitions — the app imports the virtual module, the adapter maps it:
|
|
470
368
|
|
|
471
|
-
|
|
369
|
+
```tsx
|
|
370
|
+
import { pageRoutes } from "virtual:file-routes";
|
|
371
|
+
import { fileRoutes } from "@solidjs/router/fs";
|
|
472
372
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
3. We have a reactive refetch mechanism based on key. So we can tell routes that aren't new to retrigger on action revalidation.
|
|
476
|
-
4. It will serve as a back/forward cache for browser navigation up to 5 mins. Any user based navigation or link click bypasses this cache. Revalidation or new fetch updates the cache.
|
|
373
|
+
export const Router = createRouter({ routes: fileRoutes(pageRoutes) });
|
|
374
|
+
```
|
|
477
375
|
|
|
478
|
-
|
|
376
|
+
Route files export their component as `default` and everything else as a `route` config export, which is spread into the definition. Inside a route file the pattern lives in the filename, so there's no `paths` node to witness with at the definition site — `defineFileRoute` takes the pattern string instead, types `preload`'s params from it, and validates `matchFilters` along the way. The config then doubles as the component's [`RouteProps`](#typed-route-params) witness, typing `params` from the pattern and `data` from the `preload`'s return type:
|
|
479
377
|
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
import {
|
|
483
|
-
import {
|
|
378
|
+
```tsx
|
|
379
|
+
// routes/blog/[id].tsx
|
|
380
|
+
import { int } from "@solidjs/router";
|
|
381
|
+
import { defineFileRoute } from "@solidjs/router/fs";
|
|
484
382
|
|
|
485
|
-
const
|
|
383
|
+
export const route = defineFileRoute("/blog/:id", {
|
|
384
|
+
matchFilters: { id: int },
|
|
385
|
+
preload: ({ params }) => getPost(params.id) // params.id: string
|
|
386
|
+
});
|
|
486
387
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
388
|
+
export default function Post(props: RouteProps<typeof route>) {
|
|
389
|
+
props.params.id; // string
|
|
390
|
+
props.data; // ReturnType of the preload above
|
|
490
391
|
}
|
|
491
|
-
|
|
492
|
-
// Pass it in the route definition
|
|
493
|
-
<Route path="/users/:id" component={User} preload={preloadUser} />;
|
|
494
392
|
```
|
|
495
393
|
|
|
496
|
-
|
|
394
|
+
The pattern string is a typing witness — at runtime the manifest's path (from the filename) is the source of truth. With the plugin's `types` option generating a literal declaration for the virtual module, the file paths flow into `paths` and the typed hooks like a hand-written tree — `paths.blog(42)` typechecks, filters and search schemas included, and `useParams(paths.blog)` works as usual anywhere under the route.
|
|
497
395
|
|
|
498
|
-
|
|
499
|
-
// pages/users/[id].js
|
|
500
|
-
import { getUser } from ... // the query function
|
|
396
|
+
## Typed Paths
|
|
501
397
|
|
|
502
|
-
|
|
503
|
-
const user = createAsync(() => getUser(props.params.id));
|
|
504
|
-
return <h1>{user().name}</h1>;
|
|
505
|
-
}
|
|
506
|
-
```
|
|
398
|
+
`paths` is a proxy inferred from the route tree. Property access descends into static segments, calls bind params, and it mirrors URL anatomy — params, then a search object, then a hash string:
|
|
507
399
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
getUser.keyFor(id); // returns "users[5]"
|
|
400
|
+
```tsx
|
|
401
|
+
paths.users(123) // ok — matchFilters flow into the callsite
|
|
402
|
+
paths.users(2).settings // chainable into children
|
|
403
|
+
paths.users(2, { tab: "x" }, "comments") // "/users/2?tab=x#comments"
|
|
404
|
+
paths.about() // zero-arg/search calls terminate to a plain string
|
|
405
|
+
paths() // "/" — the root
|
|
515
406
|
```
|
|
516
407
|
|
|
517
|
-
|
|
408
|
+
Every node coerces via `toString`, so nodes drop straight into `href`, `navigate()`, and `redirect()` without explicit termination. Accessing a segment that doesn't exist in the tree, or binding a param with the wrong type, is a compile error.
|
|
409
|
+
|
|
410
|
+
## Links
|
|
518
411
|
|
|
519
|
-
|
|
412
|
+
There is no link component. Use `<a>`; the router intercepts same-origin clicks through delegation and manages link state through compiler-claimed anchors — correct at creation (so late mounts under `<Show>`, `<For>`, or portals are never stale) and refreshed if a dynamic `href` changes.
|
|
520
413
|
|
|
521
|
-
|
|
414
|
+
Behavior modifiers are attributes, so they work identically in client, server-rendered, and third-party markup:
|
|
522
415
|
|
|
523
|
-
|
|
416
|
+
| attribute | description |
|
|
417
|
+
| ---------- | ------------------------------------------------------------------------------ |
|
|
418
|
+
| `replace` | Replace the history entry instead of pushing |
|
|
419
|
+
| `noscroll` | Turn off scrolling to the top after navigation |
|
|
420
|
+
| `state` | JSON string [pushed](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) onto the history stack |
|
|
421
|
+
| `preload` | Set to `"false"` to opt this link out of hover/focus preloading |
|
|
422
|
+
| `link` | Marks a router link when `explicitLinks` is enabled |
|
|
423
|
+
| `target` | Any value (e.g. `_self`) opts the anchor out of router handling |
|
|
524
424
|
|
|
525
|
-
```
|
|
526
|
-
|
|
425
|
+
```tsx
|
|
426
|
+
<a href={paths.login} replace>Log in</a>
|
|
427
|
+
<a href={paths.docs} noscroll>Docs</a>
|
|
428
|
+
<a href="https://example.com">External — untouched</a>
|
|
527
429
|
```
|
|
528
430
|
|
|
529
|
-
|
|
431
|
+
Active and pending state is styled with CSS — one vocabulary for every kind of link:
|
|
530
432
|
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
|
|
433
|
+
```css
|
|
434
|
+
nav a[aria-current="page"] { font-weight: 600; } /* exact match */
|
|
435
|
+
nav a[data-active] { color: var(--accent); } /* exact or prefix match */
|
|
436
|
+
a[data-pending] { opacity: 0.6; } /* target of in-flight navigation */
|
|
534
437
|
```
|
|
535
438
|
|
|
536
|
-
|
|
439
|
+
(The root path only ever matches exactly, so `href={paths()}` doesn't light up on every page.)
|
|
537
440
|
|
|
538
|
-
|
|
441
|
+
For component-library links that need reactive state beyond CSS, `useLinkState` is the programmatic counterpart of the attribute vocabulary:
|
|
539
442
|
|
|
540
|
-
|
|
541
|
-
|
|
443
|
+
```tsx
|
|
444
|
+
import { useLinkState } from "@solidjs/router";
|
|
542
445
|
|
|
543
|
-
|
|
544
|
-
const
|
|
446
|
+
function TabLink(props: { href: string; children: JSX.Element }) {
|
|
447
|
+
const link = useLinkState(() => props.href);
|
|
448
|
+
return (
|
|
449
|
+
<a href={props.href} class="tab" data-selected={link.active() || undefined}>
|
|
450
|
+
{props.children}
|
|
451
|
+
</a>
|
|
452
|
+
);
|
|
453
|
+
}
|
|
545
454
|
```
|
|
546
455
|
|
|
547
|
-
|
|
456
|
+
## Preload Functions
|
|
548
457
|
|
|
549
|
-
|
|
458
|
+
Even with smart caches, waterfalls happen when data fetching waits on view logic or lazy-loaded code. Preload functions start fetching data in parallel with loading the route — called when a route renders, and eagerly when links are hovered or focused.
|
|
550
459
|
|
|
551
|
-
```
|
|
552
|
-
import {
|
|
460
|
+
```tsx
|
|
461
|
+
import { lazy } from "solid-js";
|
|
553
462
|
|
|
554
|
-
|
|
555
|
-
const myAction = action(async (data) => {
|
|
556
|
-
await doMutation(data);
|
|
557
|
-
throw redirect("/", { revalidate: getUser.keyFor(data.id) }); // throw a response to do a redirect
|
|
558
|
-
});
|
|
463
|
+
const User = lazy(() => import("./pages/users/[id].js"));
|
|
559
464
|
|
|
560
|
-
|
|
561
|
-
|
|
465
|
+
function preloadUser({ params, location }) {
|
|
466
|
+
void getUser(params.id);
|
|
467
|
+
}
|
|
562
468
|
|
|
563
|
-
|
|
564
|
-
<button type="submit" formaction={myAction}></button>
|
|
469
|
+
const routes = defineRoutes([{ path: "/users/:id", component: User, preload: preloadUser }]);
|
|
565
470
|
```
|
|
566
471
|
|
|
567
|
-
|
|
472
|
+
The preload function receives:
|
|
568
473
|
|
|
569
|
-
|
|
474
|
+
| key | type | description |
|
|
475
|
+
| -------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
476
|
+
| params | object | The route parameters (same value as `useParams()` inside the route component) |
|
|
477
|
+
| location | `{ pathname, search, hash, query, state, key }` | Path information (corresponds to [`useLocation()`](#uselocation)) |
|
|
478
|
+
| intent | `"initial" \| "navigate" \| "native" \| "preload"` | Why this is being called: `initial` — first render; `navigate` — router navigation; `native` — browser back/forward; `preload` — link hover/focus, not navigating |
|
|
570
479
|
|
|
571
|
-
|
|
480
|
+
The factory-level `preload` option is the app-wide counterpart: it runs once per mount/request with the merged params of every match, and its result reaches the root render-prop as `props.data`.
|
|
572
481
|
|
|
573
|
-
|
|
574
|
-
const deleteTodo = action(async (formData: FormData) => {
|
|
575
|
-
const id = Number(formData.get("id"))
|
|
576
|
-
await api.deleteTodo(id)
|
|
577
|
-
})
|
|
482
|
+
## Data APIs
|
|
578
483
|
|
|
579
|
-
|
|
580
|
-
<input type="hidden" name="id" value={todo.id} />
|
|
581
|
-
<button type="submit">Delete</button>
|
|
582
|
-
</form>
|
|
583
|
-
```
|
|
484
|
+
These are entirely optional, but they demonstrate the power of the preload mechanism.
|
|
584
485
|
|
|
585
|
-
|
|
486
|
+
### `query`
|
|
586
487
|
|
|
587
|
-
|
|
588
|
-
const deleteTodo = action(api.deleteTodo)
|
|
488
|
+
Wrap a fetching function to dedupe calls and participate in revalidation:
|
|
589
489
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
490
|
+
```tsx
|
|
491
|
+
const getUser = query(async id => {
|
|
492
|
+
return (await fetch(`/api/users/${id}`)).json();
|
|
493
|
+
}, "users"); // query key; arguments are serialized alongside it
|
|
593
494
|
```
|
|
594
495
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
#### Notes on `<form>` implementation and SSR
|
|
496
|
+
A query:
|
|
598
497
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
```
|
|
498
|
+
1. Dedupes on the server for the lifetime of the request.
|
|
499
|
+
2. Fills a preload cache in the browser lasting 5 seconds, so hover preloads and route entry share one fetch.
|
|
500
|
+
3. Refetches reactively by key on action revalidation.
|
|
501
|
+
4. Serves as a back/forward cache for browser navigation up to 5 minutes; user-initiated navigation bypasses it.
|
|
604
502
|
|
|
605
|
-
|
|
503
|
+
Consume results directly with Solid primitives — there is no router-specific async wrapper:
|
|
606
504
|
|
|
607
|
-
|
|
505
|
+
```tsx
|
|
506
|
+
const user = createMemo(() => getUser(params.id));
|
|
507
|
+
return <h1>{user().name}</h1>;
|
|
608
508
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
const submit = useAction(myAction);
|
|
612
|
-
submit(...args);
|
|
509
|
+
// deeply reactive object data
|
|
510
|
+
const todos = createProjection(() => getTodos(), []);
|
|
613
511
|
```
|
|
614
512
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
### `useSubmission`/`useSubmissions`
|
|
513
|
+
Keys support targeted invalidation:
|
|
618
514
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
type Submission<T, U> = {
|
|
623
|
-
readonly input: T;
|
|
624
|
-
readonly result?: U;
|
|
625
|
-
readonly pending: boolean;
|
|
626
|
-
readonly url: string;
|
|
627
|
-
clear: () => void;
|
|
628
|
-
retry: () => void;
|
|
629
|
-
};
|
|
630
|
-
|
|
631
|
-
const submissions = useSubmissions(action, (input) => filter(input));
|
|
632
|
-
const submission = useSubmission(action, (input) => filter(input));
|
|
515
|
+
```ts
|
|
516
|
+
getUser.key; // "users"
|
|
517
|
+
getUser.keyFor(5); // "users[5]"
|
|
633
518
|
```
|
|
634
519
|
|
|
635
|
-
|
|
520
|
+
Revalidate with the `revalidate` export or by setting `revalidate` keys on action responses — the whole key invalidates every entry for the query, `keyFor` invalidates one.
|
|
636
521
|
|
|
637
|
-
|
|
522
|
+
### `action`
|
|
638
523
|
|
|
639
|
-
|
|
524
|
+
A router action is *an action with a URL* — Solid's mutation primitive plus URL addressability, submission tracking, and response handling. Data helpers come from the router; response helpers (`redirect`, `reload`) come from `@solidjs/web` — they're protocol-level and work without the router:
|
|
640
525
|
|
|
641
|
-
|
|
526
|
+
```tsx
|
|
527
|
+
import { action } from "@solidjs/router";
|
|
528
|
+
import { redirect } from "@solidjs/web";
|
|
529
|
+
import { paths } from "./router";
|
|
642
530
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
return user;
|
|
648
|
-
})
|
|
531
|
+
const updateUser = action(async (form: FormData) => {
|
|
532
|
+
await db.users.update(form.get("id"), form);
|
|
533
|
+
throw redirect(paths.users(form.get("id"))); // typed paths work in redirects
|
|
534
|
+
});
|
|
649
535
|
```
|
|
650
536
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
```js
|
|
656
|
-
const getTodo = query(async (id: number) => {
|
|
657
|
-
const todo = await fetchTodo(id);
|
|
658
|
-
return todo;
|
|
659
|
-
}, "todo");
|
|
537
|
+
```tsx
|
|
538
|
+
<form action={updateUser} method="post">
|
|
539
|
+
<button>Save</button>
|
|
540
|
+
</form>
|
|
660
541
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
reload({ revalidate: getTodo.keyFor(todo.id) });
|
|
664
|
-
});
|
|
542
|
+
// or
|
|
543
|
+
<button type="submit" formaction={updateUser}>Save</button>
|
|
665
544
|
```
|
|
666
545
|
|
|
667
|
-
|
|
546
|
+
Actions only work with POST requests, so put `method="post"` on your form. Submitting forms get `aria-busy="true"` automatically while the action (including its revalidation) is in flight — the same CSS story as links:
|
|
668
547
|
|
|
669
|
-
|
|
548
|
+
```css
|
|
549
|
+
form[aria-busy] button { pointer-events: none; opacity: 0.6; }
|
|
550
|
+
```
|
|
670
551
|
|
|
671
|
-
|
|
672
|
-
import { lazy } from "solid-js";
|
|
673
|
-
import { render } from "solid-js/web";
|
|
674
|
-
import { Router } from "@solidjs/router";
|
|
552
|
+
Forms work without JavaScript: a real POST, a redirect back, and the result seeded into submission state through a one-shot flash cookie. Single-flight mutations are on by default — the mutation response carries the refreshed route data in the same round trip.
|
|
675
553
|
|
|
676
|
-
|
|
677
|
-
{
|
|
678
|
-
path: "/users",
|
|
679
|
-
component: lazy(() => import("/pages/users.js")),
|
|
680
|
-
},
|
|
681
|
-
{
|
|
682
|
-
path: "/users/:id",
|
|
683
|
-
component: lazy(() => import("/pages/users/[id].js")),
|
|
684
|
-
children: [
|
|
685
|
-
{
|
|
686
|
-
path: "/",
|
|
687
|
-
component: lazy(() => import("/pages/users/[id]/index.js")),
|
|
688
|
-
},
|
|
689
|
-
{
|
|
690
|
-
path: "/settings",
|
|
691
|
-
component: lazy(() => import("/pages/users/[id]/settings.js")),
|
|
692
|
-
},
|
|
693
|
-
{
|
|
694
|
-
path: "/*all",
|
|
695
|
-
component: lazy(() => import("/pages/users/[id]/[...all].js")),
|
|
696
|
-
},
|
|
697
|
-
],
|
|
698
|
-
},
|
|
699
|
-
{
|
|
700
|
-
path: "/",
|
|
701
|
-
component: lazy(() => import("/pages/index.js")),
|
|
702
|
-
},
|
|
703
|
-
{
|
|
704
|
-
path: "/*all",
|
|
705
|
-
component: lazy(() => import("/pages/[...all].js")),
|
|
706
|
-
},
|
|
707
|
-
];
|
|
554
|
+
Delegation doesn't require the action's module on the client either. A form bound directly to a server action in a server-only module (a server component) renders a plain `action="/_server?id=...&args=..."` — a self-describing URL. On submit, the router synthesizes the invocation from it: the form data posts to that URL through the server-function transport, `.with()` arguments ride along in the query string, and submissions, `aria-busy`, redirects, revalidation, and single-flight data flow through the normal pipeline. The handler loads lazily on first such submit, so router-only bundles don't carry the data layer. The no-JS POST above remains the fallback only for clients that actually have no JavaScript. (Client-only actions — `action(fn, "name")` without `use server` — are their module's JS by definition and still require it on the client.)
|
|
708
555
|
|
|
709
|
-
|
|
710
|
-
```
|
|
556
|
+
For optimistic UI, attach owner-scoped hooks to the action and use Solid's optimistic primitives for rendered state:
|
|
711
557
|
|
|
712
|
-
|
|
558
|
+
```tsx
|
|
559
|
+
import { createOptimisticStore } from "solid-js";
|
|
560
|
+
import { action, query } from "@solidjs/router";
|
|
713
561
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
import { render } from "solid-js/web";
|
|
717
|
-
import { Router } from "@solidjs/router";
|
|
718
|
-
|
|
719
|
-
const route = {
|
|
720
|
-
path: "/",
|
|
721
|
-
component: lazy(() => import("/pages/index.js")),
|
|
722
|
-
};
|
|
562
|
+
const getTodos = query(async () => fetchTodos(), "todos");
|
|
563
|
+
const [todos, setTodos] = createOptimisticStore(() => getTodos(), []);
|
|
723
564
|
|
|
724
|
-
|
|
565
|
+
const addTodo = action(async todo => {
|
|
566
|
+
await saveTodo(todo);
|
|
567
|
+
return { ok: true, todo };
|
|
568
|
+
}, "add-todo").onSubmit(todo => {
|
|
569
|
+
setTodos(items => {
|
|
570
|
+
items.push({ ...todo, pending: true });
|
|
571
|
+
});
|
|
572
|
+
});
|
|
725
573
|
```
|
|
726
574
|
|
|
727
|
-
|
|
575
|
+
`onSubmit(...)` registers a listener in the current reactive owner — multiple components can register against the same action, and hooks are removed when their owner is disposed. `onSettled(...)` works the same way for observing completed submissions.
|
|
728
576
|
|
|
729
|
-
|
|
577
|
+
The preferred pattern is returning values and letting the client interpret the result; thrown errors are still captured on `Submission.error` as an escape hatch.
|
|
730
578
|
|
|
731
|
-
|
|
579
|
+
Actions have a `with` method (like `bind`) for typed arguments instead of hidden form fields:
|
|
732
580
|
|
|
733
|
-
```
|
|
734
|
-
|
|
581
|
+
```tsx
|
|
582
|
+
const deleteTodo = action(api.deleteTodo);
|
|
735
583
|
|
|
736
|
-
<
|
|
584
|
+
<form action={deleteTodo.with(todo.id)} method="post">
|
|
585
|
+
<button type="submit">Delete</button>
|
|
586
|
+
</form>;
|
|
737
587
|
```
|
|
738
588
|
|
|
739
|
-
|
|
589
|
+
Since form actions serialize to string attributes that must match across SSR, actions that aren't server functions need a stable name: `action(fn, "my-action")`.
|
|
740
590
|
|
|
741
|
-
|
|
591
|
+
### `useAction`
|
|
742
592
|
|
|
743
|
-
|
|
744
|
-
import { MemoryRouter } from "@solidjs/router";
|
|
593
|
+
Call an action directly instead of through a form — this is how the router context is captured. Outside a form you can pass typed data instead of `FormData`, but this requires client-side JavaScript and is not progressively enhanceable:
|
|
745
594
|
|
|
746
|
-
|
|
595
|
+
```tsx
|
|
596
|
+
const submit = useAction(myAction);
|
|
597
|
+
submit(...args);
|
|
747
598
|
```
|
|
748
599
|
|
|
749
|
-
###
|
|
750
|
-
|
|
751
|
-
For SSR you can use the static router directly or the browser Router defaults to it on the server, just pass in the url.
|
|
600
|
+
### `useSubmissions`
|
|
752
601
|
|
|
753
|
-
|
|
754
|
-
import { isServer } from "solid-js/web";
|
|
755
|
-
import { Router } from "@solidjs/router";
|
|
602
|
+
Returns settled submission records for an action — the durable history layer, not in-flight state. Useful for reading completed results, clearing old submissions, retrying, or replaying settled errors:
|
|
756
603
|
|
|
757
|
-
|
|
604
|
+
```tsx
|
|
605
|
+
const submissions = useSubmissions(action, input => filter(input));
|
|
606
|
+
const latest = submissions.at(-1);
|
|
607
|
+
// { input, result?, error, url, clear(), retry() }
|
|
758
608
|
```
|
|
759
609
|
|
|
760
|
-
|
|
610
|
+
Use Solid's `createOptimistic` / `createOptimisticStore` for in-flight UI.
|
|
761
611
|
|
|
762
|
-
|
|
612
|
+
## Typed Search Params
|
|
763
613
|
|
|
764
|
-
|
|
614
|
+
Give a route a [Standard Schema](https://github.com/standard-schema/standard-schema) validator (Valibot, Zod, ArkType, hand-rolled…) and its types flow into `paths` and `useSearchParams`:
|
|
765
615
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
| children | `JSX.Element`, `RouteDefinition`, or `RouteDefinition[]` | The route definitions |
|
|
769
|
-
| root | Component | Top level layout component |
|
|
770
|
-
| base | string | Base url to use for matching routes |
|
|
771
|
-
| actionBase | string | Root url for server actions, default: `/_server` |
|
|
772
|
-
| preload | boolean | Enables/disables preloads globally, default: `true` |
|
|
773
|
-
| explicitLinks | boolean | Disables all anchors being intercepted and instead requires `<A>`. Default: `false`. (To disable interception for a specific link, set `target` to any value, e.g. `<a target="_self">`.) |
|
|
616
|
+
```tsx
|
|
617
|
+
import * as v from "valibot";
|
|
774
618
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
619
|
+
const routes = defineRoutes([
|
|
620
|
+
{
|
|
621
|
+
path: "/search",
|
|
622
|
+
component: Search,
|
|
623
|
+
search: v.object({
|
|
624
|
+
q: v.optional(v.string(), ""),
|
|
625
|
+
page: v.optional(v.pipe(v.unknown(), v.transform(Number)), 1)
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
]);
|
|
629
|
+
```
|
|
780
630
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
| replace | boolean | If true, don't add a new entry to the browser history. (By default, the new page will be added to the browser history, so pressing the back button will take you to the previous route.) |
|
|
786
|
-
| state | unknown | [Push this value](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) to the history stack when navigating |
|
|
787
|
-
| inactiveClass | string | The class to show when the link is inactive (when the current location doesn't match the link) |
|
|
788
|
-
| activeClass | string | The class to show when the link is active |
|
|
789
|
-
| end | boolean | If `true`, only considers the link to be active when the current location matches the `href` exactly; if `false`, check if the current location _starts with_ `href` |
|
|
631
|
+
```tsx
|
|
632
|
+
const [search, setSearch] = useSearchParams(paths.search);
|
|
633
|
+
search.page; // number (parsed, not "2")
|
|
634
|
+
setSearch({ page: search.page + 1 }); // typed setter
|
|
790
635
|
|
|
791
|
-
|
|
636
|
+
<a href={paths.search({ q: "solid", page: 2 })}>Search</a>; // typed builder
|
|
637
|
+
```
|
|
792
638
|
|
|
793
|
-
|
|
639
|
+
Without a schema, `useSearchParams()` behaves as before: raw string values, merge-on-set semantics (`''`, `undefined`, and `null` remove keys), navigation-like updates with auto-scrolling disabled.
|
|
794
640
|
|
|
795
|
-
|
|
796
|
-
function getPath({ navigate, location }) {
|
|
797
|
-
// navigate is the result of calling useNavigate(); location is the result of calling useLocation().
|
|
798
|
-
// You can use those to dynamically determine a path to navigate to
|
|
799
|
-
return "/some-path";
|
|
800
|
-
}
|
|
641
|
+
## Router Config Reference
|
|
801
642
|
|
|
802
|
-
|
|
803
|
-
|
|
643
|
+
```tsx
|
|
644
|
+
createRouter(config);
|
|
804
645
|
```
|
|
805
646
|
|
|
806
|
-
|
|
647
|
+
| option | type | description |
|
|
648
|
+
| --------------- | -------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
649
|
+
| `routes` | `RouteDefinition[]` | The route tree — inline arrays infer literally; wrap extracted trees in `defineRoutes` |
|
|
650
|
+
| `base` | `string` | Base url to use for matching routes |
|
|
651
|
+
| `preload` | `RoutePreloadFunc` | App-wide preload: once per mount/request, result reaches the root render-prop as `props.data` |
|
|
652
|
+
| `history` | `RouterHistory` | History adapter; defaults to browser history on the client and the request URL on the server |
|
|
653
|
+
| `singleFlight` | `boolean` | Single-flight mutations, default `true` |
|
|
654
|
+
| `actionBase` | `string` | Root url for server actions, default `/_server` |
|
|
655
|
+
| `preloadLinks` | `boolean` | Preload route code/data on link hover and focus, default `true` |
|
|
656
|
+
| `explicitLinks` | `boolean` | Require the `link` attribute for router handling instead of intercepting all anchors, default `false` |
|
|
657
|
+
| `transformUrl` | `(url: string) => string` | Rewrite URLs before matching |
|
|
807
658
|
|
|
808
|
-
The
|
|
659
|
+
The returned instance is the provider component and carries the static surface:
|
|
809
660
|
|
|
810
|
-
|
|
|
811
|
-
|
|
|
812
|
-
|
|
|
813
|
-
|
|
|
814
|
-
|
|
|
815
|
-
|
|
|
816
|
-
| preload | `RoutePreloadFunc` | Function called during preload or when the route is navigated to. |
|
|
661
|
+
| member | description |
|
|
662
|
+
| --------- | ------------------------------------------------------------------------------------------------ |
|
|
663
|
+
| `paths` | The [typed path proxy](#typed-paths) |
|
|
664
|
+
| `match` | Pure matching against an arbitrary URL — no rendering or request context; root→leaf, `[]` if none |
|
|
665
|
+
| `routes` | The config tree |
|
|
666
|
+
| `config` | The full config — lets server integrations consume the instance directly |
|
|
817
667
|
|
|
818
668
|
## Router Primitives
|
|
819
669
|
|
|
820
|
-
|
|
670
|
+
Hooks read the live session off router context.
|
|
821
671
|
|
|
822
672
|
### useParams
|
|
823
673
|
|
|
824
|
-
Retrieves a reactive, store-like object
|
|
674
|
+
Retrieves a reactive, store-like object of the current route's path parameters. Pass a paths node for typing:
|
|
825
675
|
|
|
826
|
-
```
|
|
827
|
-
const params = useParams();
|
|
828
|
-
|
|
829
|
-
// fetch user based on the id path parameter
|
|
830
|
-
const [user] = createResource(() => params.id, fetchUser);
|
|
676
|
+
```tsx
|
|
677
|
+
const params = useParams(); // Params (strings)
|
|
678
|
+
const params = useParams(paths.users); // { id: string } — typed from the tree
|
|
831
679
|
```
|
|
832
680
|
|
|
833
|
-
|
|
681
|
+
Inside a route's own `component`/`preload`, [`defineRoute`](#typed-route-params) types `props.params` without a witness.
|
|
834
682
|
|
|
835
|
-
|
|
683
|
+
### useNavigate
|
|
836
684
|
|
|
837
|
-
|
|
838
|
-
- replace (_boolean_, default `false`): replace the history entry
|
|
839
|
-
- scroll (_boolean_, default `true`): scroll to top after navigation
|
|
840
|
-
- state (_any_, default `undefined`): pass custom state to `location.state`
|
|
685
|
+
Retrieves a method to navigate. Accepts a string or a typed path node, plus options:
|
|
841
686
|
|
|
842
|
-
|
|
687
|
+
- `resolve` (_boolean_, default `true`): resolve the path against the current route
|
|
688
|
+
- `replace` (_boolean_, default `false`): replace the history entry
|
|
689
|
+
- `scroll` (_boolean_, default `true`): scroll to top after navigation
|
|
690
|
+
- `state` (_any_): pass custom state to `location.state` (serialized with [structured clone](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm))
|
|
843
691
|
|
|
844
|
-
```
|
|
692
|
+
```tsx
|
|
845
693
|
const navigate = useNavigate();
|
|
846
|
-
|
|
847
|
-
if (unauthorized) {
|
|
848
|
-
navigate("/login", { replace: true });
|
|
849
|
-
}
|
|
694
|
+
navigate(paths.login, { replace: true });
|
|
850
695
|
```
|
|
851
696
|
|
|
697
|
+
For declarative redirects on render (the old `<Navigate>`), call it during component setup or redirect from a preload.
|
|
698
|
+
|
|
852
699
|
### useLocation
|
|
853
700
|
|
|
854
|
-
Retrieves reactive `location` object
|
|
701
|
+
Retrieves the reactive `location` object:
|
|
855
702
|
|
|
856
|
-
```
|
|
703
|
+
```tsx
|
|
857
704
|
const location = useLocation();
|
|
858
|
-
|
|
859
705
|
const pathname = createMemo(() => parsePath(location.pathname));
|
|
860
706
|
```
|
|
861
707
|
|
|
862
708
|
### useSearchParams
|
|
863
709
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
The setter method accepts an object whose entries will be merged into the current query string. Values `''`, `undefined` and `null` will remove the key from the resulting query string. Updates will behave just like a navigation and the setter accepts the same optional second parameter as `navigate` and auto-scrolling is disabled by default.
|
|
867
|
-
|
|
868
|
-
```js
|
|
869
|
-
const [searchParams, setSearchParams] = useSearchParams();
|
|
870
|
-
|
|
871
|
-
return (
|
|
872
|
-
<div>
|
|
873
|
-
<span>Page: {searchParams.page}</span>
|
|
874
|
-
<button
|
|
875
|
-
onClick={() =>
|
|
876
|
-
setSearchParams({ page: (parseInt(searchParams.page) || 0) + 1 })
|
|
877
|
-
}
|
|
878
|
-
>
|
|
879
|
-
Next Page
|
|
880
|
-
</button>
|
|
881
|
-
</div>
|
|
882
|
-
);
|
|
883
|
-
```
|
|
710
|
+
See [Typed Search Params](#typed-search-params). Reads are proxied — access properties to subscribe.
|
|
884
711
|
|
|
885
712
|
### useIsRouting
|
|
886
713
|
|
|
887
|
-
|
|
714
|
+
A signal indicating whether the router is processing a navigation — useful for pending UI while the next route and its data settle:
|
|
888
715
|
|
|
889
|
-
```
|
|
716
|
+
```tsx
|
|
890
717
|
const isRouting = useIsRouting();
|
|
891
|
-
|
|
892
|
-
return (
|
|
893
|
-
<div classList={{ "grey-out": isRouting() }}>
|
|
894
|
-
<MyAwesomeContent />
|
|
895
|
-
</div>
|
|
896
|
-
);
|
|
718
|
+
return <div classList={{ "grey-out": isRouting() }}>...</div>;
|
|
897
719
|
```
|
|
898
720
|
|
|
899
721
|
### useMatch
|
|
900
722
|
|
|
901
|
-
|
|
723
|
+
Tests a path *pattern you supply* against the current location; returns a memo of match information or `undefined`. It never consults the route tree — the pattern doesn't have to correspond to a defined route. The match's `params` are typed from the pattern, and a typed path node works too (a concrete URL — useful for "am I here" checks):
|
|
902
724
|
|
|
903
|
-
```
|
|
904
|
-
const match = useMatch(() =>
|
|
725
|
+
```tsx
|
|
726
|
+
const match = useMatch(() => "/admin/*rest");
|
|
727
|
+
match()?.params.rest; // string
|
|
728
|
+
return <Show when={match()}>...</Show>;
|
|
905
729
|
|
|
906
|
-
|
|
730
|
+
const here = useMatch(() => paths.users(2));
|
|
907
731
|
```
|
|
908
732
|
|
|
909
|
-
###
|
|
733
|
+
### useRouteMatches
|
|
910
734
|
|
|
911
|
-
|
|
735
|
+
Returns an accessor of the router's *resolved* matches for the current location — the chain of route definitions producing the current render, outermost first. This is the counterpart to `useMatch`: one reflects the route tree, the other tests a pattern. Useful for reading `info` metadata:
|
|
912
736
|
|
|
913
|
-
|
|
737
|
+
```tsx
|
|
738
|
+
const matches = useRouteMatches();
|
|
739
|
+
const breadcrumbs = createMemo(() => matches().map(m => m.route.info?.breadcrumb));
|
|
740
|
+
```
|
|
914
741
|
|
|
915
|
-
|
|
916
|
-
const matches = useCurrentMatches();
|
|
742
|
+
`info` is freeform by default; augment `RouteInfo` to type it app-wide — declared keys are checked at route definitions and typed on reads:
|
|
917
743
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
744
|
+
```ts
|
|
745
|
+
declare module "@solidjs/router" {
|
|
746
|
+
interface RouteInfo {
|
|
747
|
+
breadcrumb?: string;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
921
750
|
```
|
|
922
751
|
|
|
923
752
|
### usePreloadRoute
|
|
924
753
|
|
|
925
|
-
|
|
754
|
+
Returns a function to preload a route manually — the same work link hover/focus triggers automatically. Accepts strings, URLs, and typed path nodes:
|
|
926
755
|
|
|
927
|
-
```
|
|
756
|
+
```tsx
|
|
928
757
|
const preload = usePreloadRoute();
|
|
929
|
-
|
|
930
|
-
preload(`/users/settings`, { preloadData: true });
|
|
758
|
+
preload(paths.users(2).settings, { preloadData: true });
|
|
931
759
|
```
|
|
932
760
|
|
|
933
|
-
###
|
|
761
|
+
### useLinkState
|
|
934
762
|
|
|
935
|
-
`
|
|
763
|
+
Reactive `active`/`current`/`pending` state for [custom link components](#links).
|
|
936
764
|
|
|
937
|
-
|
|
938
|
-
- to (_string | number_): path passed to `navigate`.
|
|
939
|
-
- options (_NavigateOptions_): options passed to `navigate`.
|
|
940
|
-
- preventDefault (_function_): call to block the route change.
|
|
941
|
-
- defaultPrevented (_readonly boolean_): `true` if any previously called leave handlers called `preventDefault`.
|
|
942
|
-
- retry (_function_, _force?: boolean_ ): call to retry the same navigation, perhaps after confirming with the user. Pass `true` to skip running the leave handlers again (i.e. force navigate without confirming).
|
|
765
|
+
### useBeforeLeave
|
|
943
766
|
|
|
944
|
-
|
|
767
|
+
Takes a function called before leaving a route. The blocking machinery is installed lazily on first use, so apps that never call `useBeforeLeave` don't pay for it in their bundle. The handler receives:
|
|
945
768
|
|
|
946
|
-
|
|
769
|
+
- `from` (_Location_): current location (before change)
|
|
770
|
+
- `to` (_string | number_): path passed to `navigate`
|
|
771
|
+
- `options` (_NavigateOptions_): options passed to `navigate`
|
|
772
|
+
- `preventDefault()`: call to block the route change
|
|
773
|
+
- `defaultPrevented` (_readonly boolean_): `true` if any previous handler called `preventDefault`
|
|
774
|
+
- `retry(force?)`: retry the navigation, e.g. after confirming with the user; pass `true` to skip re-running leave handlers
|
|
775
|
+
|
|
776
|
+
```tsx
|
|
947
777
|
useBeforeLeave((e: BeforeLeaveEventArgs) => {
|
|
948
778
|
if (form.isDirty && !e.defaultPrevented) {
|
|
949
|
-
// preventDefault to block immediately and prompt user async
|
|
950
779
|
e.preventDefault();
|
|
951
780
|
setTimeout(() => {
|
|
952
781
|
if (window.confirm("Discard unsaved changes - are you sure?")) {
|
|
953
|
-
// user wants to proceed anyway so retry with force=true
|
|
954
782
|
e.retry(true);
|
|
955
783
|
}
|
|
956
784
|
}, 100);
|
|
@@ -958,73 +786,154 @@ useBeforeLeave((e: BeforeLeaveEventArgs) => {
|
|
|
958
786
|
});
|
|
959
787
|
```
|
|
960
788
|
|
|
961
|
-
##
|
|
789
|
+
## Other Environments
|
|
962
790
|
|
|
963
|
-
|
|
791
|
+
History adapters are plain imports, so unused ones never enter your bundle:
|
|
964
792
|
|
|
965
|
-
|
|
793
|
+
```tsx
|
|
794
|
+
import { createRouter, hashHistory, memoryHistory } from "@solidjs/router";
|
|
966
795
|
|
|
967
|
-
|
|
796
|
+
// hash mode
|
|
797
|
+
const Router = createRouter({ routes, history: hashHistory() });
|
|
968
798
|
|
|
969
|
-
|
|
799
|
+
// tests and non-browser environments
|
|
800
|
+
const Router = createRouter({ routes, history: memoryHistory("/users/1") });
|
|
801
|
+
```
|
|
970
802
|
|
|
971
|
-
|
|
803
|
+
### Environments without Proxy
|
|
972
804
|
|
|
973
|
-
|
|
805
|
+
On runtimes without `Proxy` support (some older smart TVs), the core router still works: typed `paths` are built lazily so they only require `Proxy` if you access them, and `params`/`location.query` can be swapped to a `Proxy`-free implementation through the history adapter's `paramsWrapper`/`queryWrapper` utils:
|
|
974
806
|
|
|
975
|
-
|
|
807
|
+
```tsx
|
|
808
|
+
const base = browserHistory();
|
|
809
|
+
const history = {
|
|
810
|
+
...base,
|
|
811
|
+
// wrappers build objects with defined getters instead of a Proxy
|
|
812
|
+
utils: { ...base.utils, paramsWrapper, queryWrapper }
|
|
813
|
+
};
|
|
814
|
+
const Router = createRouter({ routes, history });
|
|
815
|
+
```
|
|
976
816
|
|
|
977
|
-
|
|
817
|
+
On the server the request URL drives rendering automatically. Without a request event (SSG scripts, server-side tests), the configured history adapter provides the location, so `memoryHistory("/page")` renders that page isomorphically.
|
|
978
818
|
|
|
979
|
-
|
|
819
|
+
The instance also matches arbitrary URLs anywhere — server middleware, sitemap generation, tests — with no rendering involved:
|
|
980
820
|
|
|
981
|
-
|
|
821
|
+
```tsx
|
|
822
|
+
import { Router } from "./router";
|
|
982
823
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
824
|
+
Router.match("/users/2/settings?tab=x");
|
|
825
|
+
// [
|
|
826
|
+
// { path: "/users/:id", match: "/users/2", params: { id: "2" } },
|
|
827
|
+
// { path: "/settings", match: "/users/2/settings", params: {} }
|
|
828
|
+
// ]
|
|
829
|
+
```
|
|
986
830
|
|
|
987
|
-
|
|
831
|
+
## Server Integration
|
|
988
832
|
|
|
989
|
-
|
|
990
|
-
function preloadUser({ params, location }) {
|
|
991
|
-
const [user] = createResource(() => params.id, fetchUser);
|
|
992
|
-
return user;
|
|
993
|
-
}
|
|
833
|
+
Framework handler wiring lives in `@solidjs/router/server`. The integration accepts the router instance directly — its routes, base, and preload are the single source of truth:
|
|
994
834
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
835
|
+
```tsx
|
|
836
|
+
import { createFlightDataCollector } from "@solidjs/router/server";
|
|
837
|
+
import { Router } from "./app/router";
|
|
838
|
+
|
|
839
|
+
const collectFlightData = createFlightDataCollector(Router);
|
|
999
840
|
```
|
|
1000
841
|
|
|
1001
|
-
|
|
842
|
+
`createFlightDataCollector` produces the single-flight hook: after a mutation it reruns the route data the mutation invalidated for the page the client is on (or is redirected to), folding fresh data into the same response. This policy previously lived inside SolidStart; the router now owns it, so custom server setups get single-flight mutations without a framework.
|
|
1002
843
|
|
|
1003
|
-
|
|
1004
|
-
function User(props) {
|
|
1005
|
-
<UserContext.Provider value={props.data}>
|
|
1006
|
-
{/* my component content */}
|
|
1007
|
-
</UserContext.Provider>;
|
|
1008
|
-
}
|
|
844
|
+
The no-JS form convention needs no wiring at all: the server function runtime answers form posts made without the client runtime by redirecting back with the outcome in a one-shot flash cookie, and the router's SSR reads it into submission state. To configure it (e.g. a base path), pass `createNoJSHandler(options)` from `@solidjs/web/server-functions/server` as the handler's `handleNoJS`.
|
|
1009
845
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
846
|
+
## Migration from 0.x
|
|
847
|
+
|
|
848
|
+
This guide maps from the stable 0.x releases (Solid 1). 1.0 removes the component-based API — the `createRouter` factory is the only way to set up the router, and plain `<a>` elements are the only link primitive. 1.0 also targets Solid 2, so the async data patterns change alongside the router API.
|
|
849
|
+
|
|
850
|
+
### Router components → `createRouter`
|
|
851
|
+
|
|
852
|
+
```tsx
|
|
853
|
+
// 0.x
|
|
854
|
+
<Router root={App}>
|
|
855
|
+
<Route path="/users" component={Users} />
|
|
856
|
+
<Route path="/users/:id" component={User} />
|
|
857
|
+
</Router>
|
|
858
|
+
|
|
859
|
+
// 1.0
|
|
860
|
+
const Router = createRouter({
|
|
861
|
+
routes: [
|
|
862
|
+
{ path: "/users", component: Users },
|
|
863
|
+
{ path: "/users/:id", component: User }
|
|
864
|
+
]
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
<Router>{props => <App {...props} />}</Router>
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
- `<HashRouter>` → `createRouter({ routes, history: hashHistory() })`
|
|
871
|
+
- `<MemoryRouter>` / `createMemoryHistory` → `createRouter({ routes, history: memoryHistory("/initial") })`
|
|
872
|
+
- `<StaticRouter url>` / `<Router url>` for SSR → automatic from the request URL; without a request event, pass `memoryHistory(url)`
|
|
873
|
+
- `root` prop → the render-prop child; `rootPreload` → the factory's `preload` option
|
|
874
|
+
|
|
875
|
+
### JSX `<Route>` → config objects
|
|
876
|
+
|
|
877
|
+
Route props map 1:1 onto definition keys (`path`, `component`, `preload`, `matchFilters`, `info`); nesting becomes `children` arrays. Wrap extracted route trees in `defineRoutes` to get typed `paths`. File-based routing generates config.
|
|
878
|
+
|
|
879
|
+
### `<A>` → plain `<a>`
|
|
880
|
+
|
|
881
|
+
- `<A href replace noScroll state>` → `<a href replace noscroll state>` (attributes, all lowercase)
|
|
882
|
+
- `activeClass` / `inactiveClass` → CSS attribute selectors on `[data-active]` / `[aria-current="page"]`
|
|
883
|
+
- `end` → style exact matches with `[aria-current="page"]` instead of `[data-active]`; the root path already only matches exactly
|
|
884
|
+
- Route-relative hrefs → typed `paths`; `useResolvedPath` / `useHref` remain for manual resolution
|
|
885
|
+
- Custom link components → `useLinkState`
|
|
886
|
+
|
|
887
|
+
### Removed and renamed
|
|
888
|
+
|
|
889
|
+
- `<Navigate>` → call `useNavigate()` during component setup, or redirect from a preload
|
|
890
|
+
- `useCurrentMatches` → `useRouteMatches` (same behavior)
|
|
891
|
+
- `redirect` / `reload` → import from `@solidjs/web`; they're protocol-level and work without the router
|
|
892
|
+
- `json(data, init)` → `respond(data, init)` from `@solidjs/web`
|
|
893
|
+
- `cache` (deprecated alias) → `query`
|
|
894
|
+
|
|
895
|
+
### Data APIs (Solid 2)
|
|
896
|
+
|
|
897
|
+
- `createAsync` / `createAsyncStore` are gone — read `query()` results with Solid 2 primitives: `createMemo`, `createProjection`, `createOptimistic`, `createOptimisticStore`.
|
|
898
|
+
|
|
899
|
+
```tsx
|
|
900
|
+
// 0.x
|
|
901
|
+
const user = createAsync(() => getUser(params.id));
|
|
902
|
+
|
|
903
|
+
// 1.0
|
|
904
|
+
const user = createMemo(() => getUser(params.id));
|
|
1015
905
|
```
|
|
1016
906
|
|
|
907
|
+
- `query()` stays the source of truth for cached reads and invalidation.
|
|
908
|
+
- `useSubmission` (singular) is gone, and submissions are now settled history rather than in-flight state. Pending/optimistic UI moves to Solid's optimistic primitives fed by the action's `.onSubmit(...)` hook; read settled results with `useSubmissions()` and select the latest with `.at(-1)`.
|
|
909
|
+
|
|
910
|
+
```tsx
|
|
911
|
+
// 0.x — read in-flight state off the submission
|
|
912
|
+
const submitting = useSubmission(addTodo);
|
|
913
|
+
<span>{submitting.pending && "Saving..."}</span>;
|
|
914
|
+
|
|
915
|
+
// 1.0 — optimistic primitives own in-flight state
|
|
916
|
+
const [todos, setTodos] = createOptimisticStore(() => getTodos(), []);
|
|
917
|
+
const addTodo = action(saveTodo).onSubmit(todo =>
|
|
918
|
+
setTodos(items => {
|
|
919
|
+
items.push({ ...todo, pending: true });
|
|
920
|
+
})
|
|
921
|
+
);
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
- Action lifecycle centers on instance methods: `.onSubmit(...)` for owner-scoped optimistic work, `.onSettled(...)` for observing completions. Returned values are the expected result channel; thrown errors land on `Submission.error`.
|
|
925
|
+
|
|
1017
926
|
## SPAs in Deployed Environments
|
|
1018
927
|
|
|
1019
|
-
When deploying
|
|
928
|
+
When deploying a client-side-routed application without server-side rendering, you need to handle redirects to your index page so that loading other URLs doesn't return a 404.
|
|
1020
929
|
|
|
1021
|
-
|
|
930
|
+
On Netlify, create a `_redirects` file:
|
|
1022
931
|
|
|
1023
932
|
```sh
|
|
1024
933
|
/* /index.html 200
|
|
1025
934
|
```
|
|
1026
935
|
|
|
1027
|
-
On Vercel
|
|
936
|
+
On Vercel, add a rewrites section to `vercel.json`:
|
|
1028
937
|
|
|
1029
938
|
```json
|
|
1030
939
|
{
|