@solidjs/router 0.17.0-next.6 → 1.0.0-next.8
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 +526 -716
- package/dist/claims.d.ts +21 -0
- package/dist/claims.js +115 -0
- package/dist/data/action.d.ts +15 -0
- package/dist/data/action.js +127 -12
- package/dist/data/events.d.ts +8 -0
- package/dist/data/events.js +23 -22
- package/dist/data/flash.d.ts +1 -5
- package/dist/data/flash.js +10 -18
- package/dist/data/flashCookie.d.ts +7 -0
- package/dist/data/flashCookie.js +20 -0
- package/dist/data/serverForms.d.ts +1 -0
- package/dist/data/serverForms.js +5 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1736 -1157
- 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 +10 -21
- package/dist/routers/components.jsx +29 -51
- package/dist/routers/factory.d.ts +45 -0
- package/dist/routers/factory.jsx +143 -0
- package/dist/routers/history.d.ts +24 -0
- package/dist/routers/history.js +180 -0
- package/dist/routers/index.d.ts +4 -11
- package/dist/routers/index.js +2 -6
- package/dist/routing.d.ts +79 -52
- package/dist/routing.js +296 -127
- package/dist/server.d.ts +25 -15
- package/dist/server.js +43 -7
- package/dist/types.d.ts +74 -5
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +2 -0
- package/package.json +6 -6
- package/dist/components.d.ts +0 -31
- package/dist/components.jsx +0 -46
- 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 -9
- package/dist/routers/Router.js +0 -45
- 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 -40
package/README.md
CHANGED
|
@@ -10,1027 +10,837 @@
|
|
|
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
|
+
- [Match Filters](#match-filters)
|
|
33
|
+
- [Optional Parameters](#optional-parameters)
|
|
34
|
+
- [Wildcard Routes](#wildcard-routes)
|
|
35
|
+
- [Multiple Paths](#multiple-paths)
|
|
36
|
+
- [Nested Routes](#nested-routes)
|
|
37
|
+
- [Lazy Route Subtrees](#lazy-route-subtrees)
|
|
38
|
+
- [Typed Paths](#typed-paths)
|
|
39
|
+
- [Links](#links)
|
|
40
|
+
- [Preload Functions](#preload-functions)
|
|
41
41
|
- [Data APIs](#data-apis)
|
|
42
|
-
- [
|
|
43
|
-
- [
|
|
42
|
+
- [Typed Search Params](#typed-search-params)
|
|
43
|
+
- [Router Config Reference](#router-config-reference)
|
|
44
44
|
- [Router Primitives](#router-primitives)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- [useSearchParams](#usesearchparams)
|
|
49
|
-
- [useIsRouting](#useisrouting)
|
|
50
|
-
- [useMatch](#usematch)
|
|
51
|
-
- [useCurrentMatches](#useCurrentMatches)
|
|
52
|
-
- [useBeforeLeave](#usebeforeleave)
|
|
45
|
+
- [Other Environments](#other-environments)
|
|
46
|
+
- [Server Integration](#server-integration)
|
|
47
|
+
- [Migration from 0.x](#migration-from-0x)
|
|
53
48
|
- [SPAs in Deployed Environments](#spas-in-deployed-environments)
|
|
54
49
|
|
|
55
50
|
## Getting Started
|
|
56
51
|
|
|
57
|
-
### Set Up the Router
|
|
58
|
-
|
|
59
52
|
```bash
|
|
60
53
|
# use preferred package manager
|
|
61
54
|
npm add @solidjs/router
|
|
62
55
|
```
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```jsx
|
|
67
|
-
import { render } from "@solidjs/web";
|
|
68
|
-
import { Router } from "@solidjs/router";
|
|
69
|
-
|
|
70
|
-
render(() => <Router />, document.getElementById("app"));
|
|
71
|
-
```
|
|
57
|
+
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:
|
|
72
58
|
|
|
73
|
-
|
|
59
|
+
```tsx
|
|
60
|
+
// app/router.ts
|
|
61
|
+
import { lazy } from "solid-js";
|
|
62
|
+
import { createRouter } from "@solidjs/router";
|
|
74
63
|
|
|
75
|
-
|
|
64
|
+
export const Router = createRouter({
|
|
65
|
+
routes: [
|
|
66
|
+
{ path: "/", component: lazy(() => import("./pages/Home")) },
|
|
67
|
+
{ path: "/about", component: lazy(() => import("./pages/About")) },
|
|
68
|
+
{
|
|
69
|
+
path: "/users/:id",
|
|
70
|
+
component: lazy(() => import("./pages/User")),
|
|
71
|
+
children: [
|
|
72
|
+
{ path: "/", component: lazy(() => import("./pages/UserOverview")) },
|
|
73
|
+
{ path: "/settings", component: lazy(() => import("./pages/UserSettings")) }
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
{ path: "*404", component: lazy(() => import("./pages/NotFound")) }
|
|
77
|
+
]
|
|
78
|
+
});
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
export const { paths } = Router;
|
|
81
|
+
```
|
|
78
82
|
|
|
79
|
-
|
|
83
|
+
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.
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
import { render } from "@solidjs/web";
|
|
83
|
-
import { Router, Route } from "@solidjs/router";
|
|
85
|
+
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:
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
```tsx
|
|
88
|
+
// features/admin/routes.ts
|
|
89
|
+
export const adminRoutes = defineRoutes([
|
|
90
|
+
{ path: "/admin", component: Admin, children: [/* ... */] }
|
|
91
|
+
]);
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<Router>
|
|
91
|
-
<Route path="/users" component={Users} />
|
|
92
|
-
<Route path="/" component={Home} />
|
|
93
|
-
</Router>
|
|
94
|
-
),
|
|
95
|
-
document.getElementById("app")
|
|
96
|
-
);
|
|
93
|
+
// app/router.ts
|
|
94
|
+
export const Router = createRouter({ routes: [...appRoutes, ...adminRoutes] });
|
|
97
95
|
```
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
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:
|
|
100
98
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
```jsx
|
|
99
|
+
```tsx
|
|
100
|
+
// app/index.tsx
|
|
104
101
|
import { render } from "@solidjs/web";
|
|
105
|
-
import { Router
|
|
106
|
-
|
|
107
|
-
import Home from "./pages/Home";
|
|
108
|
-
import Users from "./pages/Users";
|
|
109
|
-
|
|
110
|
-
const App = (props) => (
|
|
111
|
-
<>
|
|
112
|
-
<h1>My Site with lots of pages</h1>
|
|
113
|
-
{props.children}
|
|
114
|
-
</>
|
|
115
|
-
);
|
|
102
|
+
import { Router } from "./router";
|
|
116
103
|
|
|
117
104
|
render(
|
|
118
105
|
() => (
|
|
119
|
-
<Router
|
|
120
|
-
|
|
121
|
-
|
|
106
|
+
<Router>
|
|
107
|
+
{props => (
|
|
108
|
+
<>
|
|
109
|
+
<h1>My Site with lots of pages</h1>
|
|
110
|
+
{props.children}
|
|
111
|
+
</>
|
|
112
|
+
)}
|
|
122
113
|
</Router>
|
|
123
114
|
),
|
|
124
|
-
document.getElementById("app")
|
|
115
|
+
document.getElementById("app")!
|
|
125
116
|
);
|
|
126
117
|
```
|
|
127
118
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
We can create catch-all routes for pages not found at any nested level of the router. We use `*` and optionally the name of a parameter to retrieve the rest of the path.
|
|
131
|
-
|
|
132
|
-
```jsx
|
|
133
|
-
import { render } from "@solidjs/web";
|
|
134
|
-
import { Router, Route } from "@solidjs/router";
|
|
135
|
-
|
|
136
|
-
import Home from "./pages/Home";
|
|
137
|
-
import Users from "./pages/Users";
|
|
138
|
-
import NotFound from "./pages/404";
|
|
119
|
+
Links are plain anchors. Typed path nodes coerce to strings on the attribute, and the router intercepts clicks through delegation:
|
|
139
120
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
<h1>My Site with lots of pages</h1>
|
|
143
|
-
{props.children}
|
|
144
|
-
</>
|
|
145
|
-
);
|
|
121
|
+
```tsx
|
|
122
|
+
import { paths } from "./router";
|
|
146
123
|
|
|
147
|
-
|
|
148
|
-
()
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
<Route path="*404" component={NotFound} />
|
|
153
|
-
</Router>
|
|
154
|
-
),
|
|
155
|
-
document.getElementById("app")
|
|
156
|
-
);
|
|
124
|
+
<nav>
|
|
125
|
+
<a href={paths()}>Home</a>
|
|
126
|
+
<a href={paths.about}>About</a>
|
|
127
|
+
<a href={paths.users(user.id).settings}>Settings</a>
|
|
128
|
+
</nav>;
|
|
157
129
|
```
|
|
158
130
|
|
|
159
|
-
|
|
131
|
+
## The Mental Model: Instance vs Hooks
|
|
160
132
|
|
|
161
|
-
|
|
133
|
+
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.**
|
|
162
134
|
|
|
163
|
-
|
|
164
|
-
import { lazy } from "solid-js";
|
|
165
|
-
import { render } from "@solidjs/web";
|
|
166
|
-
import { Router, Route } from "@solidjs/router";
|
|
135
|
+
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.
|
|
167
136
|
|
|
168
|
-
|
|
169
|
-
|
|
137
|
+
| Instance — facts about the *app* | Hooks — facts about the *session* |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| `paths` — how to spell URLs | `useLocation`, `useParams` — where am I |
|
|
140
|
+
| `match(url)` — how would a URL match | `useNavigate`, `usePreloadRoute` — move / warm |
|
|
141
|
+
| `routes`, `config` — what exists | `useIsRouting`, `useRouteMatches`, `useSearchParams` — live state |
|
|
170
142
|
|
|
171
|
-
|
|
172
|
-
<>
|
|
173
|
-
<h1>My Site with lots of pages</h1>
|
|
174
|
-
{props.children}
|
|
175
|
-
</>
|
|
176
|
-
);
|
|
143
|
+
They compose as noun and verb — the instance supplies a typed URL, the hook acts on the current session:
|
|
177
144
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
</Router>
|
|
184
|
-
),
|
|
185
|
-
document.getElementById("app")
|
|
186
|
-
);
|
|
145
|
+
```tsx
|
|
146
|
+
const navigate = useNavigate();
|
|
147
|
+
navigate(paths.users(2)); // verb(noun)
|
|
148
|
+
|
|
149
|
+
const params = useParams(paths.users); // hook, typed by the instance
|
|
187
150
|
```
|
|
188
151
|
|
|
189
|
-
|
|
152
|
+
**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.
|
|
190
153
|
|
|
191
|
-
|
|
154
|
+
## Route Definitions
|
|
192
155
|
|
|
193
|
-
|
|
194
|
-
import { lazy } from "solid-js";
|
|
195
|
-
import { render } from "@solidjs/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
|
-
);
|
|
156
|
+
A route definition supports:
|
|
211
157
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## Dynamic Routes
|
|
158
|
+
| key | type | description |
|
|
159
|
+
| -------------- | --------------------------------------- | ------------------------------------------------------------------ |
|
|
160
|
+
| `path` | `string \| string[]` | Path partial for this route segment |
|
|
161
|
+
| `component` | `Component` | Component rendered for the matched segment |
|
|
162
|
+
| `children` | `RouteDefinition \| RouteDefinition[] \| () => Promise<...>` | Nested route definitions, or a thunk for a [lazy subtree](#lazy-route-subtrees) |
|
|
163
|
+
| `preload` | `RoutePreloadFunc` | Called on preload intent (hover/focus) and navigation |
|
|
164
|
+
| `matchFilters` | `MatchFilters` | Additional constraints for matching parameters |
|
|
165
|
+
| `search` | `StandardSchemaV1` | Search-param validator; its types flow into `paths` and hooks |
|
|
166
|
+
| `info` | `Record<string, any>` | Arbitrary metadata, readable via `useRouteMatches` |
|
|
224
167
|
|
|
225
|
-
|
|
168
|
+
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.
|
|
226
169
|
|
|
227
|
-
|
|
228
|
-
import { lazy } from "solid-js";
|
|
229
|
-
import { render } from "@solidjs/web";
|
|
230
|
-
import { Router, Route } from "@solidjs/router";
|
|
170
|
+
### Dynamic Routes
|
|
231
171
|
|
|
232
|
-
|
|
233
|
-
const User = lazy(() => import("./pages/User"));
|
|
234
|
-
const Home = lazy(() => import("./pages/Home"));
|
|
172
|
+
Treat part of the path as a parameter with a colon:
|
|
235
173
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
<Route path="/" component={Home} />
|
|
242
|
-
</Router>
|
|
243
|
-
),
|
|
244
|
-
document.getElementById("app")
|
|
245
|
-
);
|
|
174
|
+
```tsx
|
|
175
|
+
const routes = defineRoutes([
|
|
176
|
+
{ path: "/users", component: Users },
|
|
177
|
+
{ path: "/users/:id", component: User }
|
|
178
|
+
]);
|
|
246
179
|
```
|
|
247
180
|
|
|
248
|
-
|
|
181
|
+
As long as the URL fits the pattern, the `User` component shows, and `id` is available via `useParams`.
|
|
249
182
|
|
|
250
|
-
|
|
183
|
+
**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>`:
|
|
251
184
|
|
|
252
|
-
|
|
253
|
-
Routes that share the same path match will be treated as the same route. If you want to force re-render you can wrap your component in a keyed `<Show>` like:
|
|
254
|
-
|
|
255
|
-
```jsx
|
|
185
|
+
```tsx
|
|
256
186
|
<Show when={params.something} keyed>
|
|
257
187
|
<MyComponent />
|
|
258
188
|
</Show>
|
|
259
189
|
```
|
|
260
190
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
Each path parameter can be validated using a `MatchFilter`.
|
|
264
|
-
This allows for more complex routing descriptions than just checking the presence of a parameter.
|
|
191
|
+
### Match Filters
|
|
265
192
|
|
|
266
|
-
|
|
267
|
-
import { lazy } from "solid-js";
|
|
268
|
-
import { render } from "@solidjs/web";
|
|
269
|
-
import { Router, Route } from "@solidjs/router";
|
|
270
|
-
import type { MatchFilters } from "@solidjs/router";
|
|
193
|
+
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
194
|
|
|
272
|
-
|
|
195
|
+
```tsx
|
|
196
|
+
import { int, type MatchFilters } from "@solidjs/router";
|
|
273
197
|
|
|
274
198
|
const filters: MatchFilters = {
|
|
275
|
-
parent: ["mom", "dad"],
|
|
276
|
-
id: /^\d+$/,
|
|
277
|
-
withHtmlExtension: (v: string) => v.length > 5 && v.endsWith(".html")
|
|
199
|
+
parent: ["mom", "dad"], // enum values
|
|
200
|
+
id: /^\d+$/, // only numbers
|
|
201
|
+
withHtmlExtension: (v: string) => v.length > 5 && v.endsWith(".html")
|
|
278
202
|
};
|
|
279
203
|
|
|
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
|
-
);
|
|
204
|
+
const routes = defineRoutes([
|
|
205
|
+
{ path: "/users/:parent/:id/:withHtmlExtension", component: User, matchFilters: filters }
|
|
206
|
+
]);
|
|
292
207
|
```
|
|
293
208
|
|
|
294
|
-
|
|
295
|
-
If the validation fails, the route will not match.
|
|
209
|
+
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
210
|
|
|
297
|
-
|
|
211
|
+
The built-in `int` filter is *typed*: it constrains matching to integers at runtime and types the param as `number` at `paths` callsites:
|
|
298
212
|
|
|
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`.
|
|
213
|
+
```tsx
|
|
214
|
+
{ path: "/users/:id", matchFilters: { id: int }, component: User }
|
|
304
215
|
|
|
305
|
-
|
|
216
|
+
paths.users(123); // ok
|
|
217
|
+
paths.users("abc"); // type error
|
|
218
|
+
```
|
|
306
219
|
|
|
307
220
|
### Optional Parameters
|
|
308
221
|
|
|
309
|
-
|
|
222
|
+
Add a question mark to make a parameter optional:
|
|
310
223
|
|
|
311
|
-
```
|
|
224
|
+
```tsx
|
|
312
225
|
// Matches stories and stories/123 but not stories/123/comments
|
|
313
|
-
|
|
226
|
+
{ path: "/stories/:id?", component: Stories }
|
|
314
227
|
```
|
|
315
228
|
|
|
316
229
|
### Wildcard Routes
|
|
317
230
|
|
|
318
|
-
|
|
231
|
+
Use `*` to match any remainder of the path, optionally naming it to expose it as a parameter:
|
|
319
232
|
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
|
|
233
|
+
```tsx
|
|
234
|
+
{ path: "foo/*", component: Foo } // matches foo/, foo/a, foo/a/b/c
|
|
235
|
+
{ path: "foo/*any", component: Foo } // rest of the path available as params.any
|
|
323
236
|
```
|
|
324
237
|
|
|
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.
|
|
238
|
+
The wildcard token must be the last part of the path; `foo/*any/bar` won't create any routes.
|
|
332
239
|
|
|
333
240
|
### Multiple Paths
|
|
334
241
|
|
|
335
|
-
|
|
242
|
+
An array of paths lets a route stay mounted (no re-render) when switching between locations it matches:
|
|
336
243
|
|
|
337
|
-
```
|
|
338
|
-
// Navigating from login to register does not
|
|
339
|
-
|
|
244
|
+
```tsx
|
|
245
|
+
// Navigating from login to register does not re-render Login
|
|
246
|
+
{ path: ["login", "register"], component: Login }
|
|
340
247
|
```
|
|
341
248
|
|
|
342
|
-
|
|
249
|
+
### Nested Routes
|
|
343
250
|
|
|
344
|
-
|
|
251
|
+
Only leaf nodes become routes. A parent with a `component` wraps its children, which render where the parent places `props.children`:
|
|
345
252
|
|
|
346
|
-
```
|
|
347
|
-
<Route path="/users/:id" component={User} />
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
```jsx
|
|
351
|
-
<Route path="/users">
|
|
352
|
-
<Route path="/:id" component={User} />
|
|
353
|
-
</Route>
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
`/users/:id` renders the `<User/>` component, and `/users/` is an empty route.
|
|
357
|
-
|
|
358
|
-
Only leaf Route nodes (innermost `Route` components) are given a route. If you want to make the parent its own route, you have to specify it separately:
|
|
359
|
-
|
|
360
|
-
```jsx
|
|
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
|
|
253
|
+
```tsx
|
|
380
254
|
function PageWrapper(props) {
|
|
381
255
|
return (
|
|
382
256
|
<div>
|
|
383
|
-
<h1>
|
|
257
|
+
<h1>We love our users!</h1>
|
|
384
258
|
{props.children}
|
|
385
|
-
<
|
|
259
|
+
<a href={paths()}>Back Home</a>
|
|
386
260
|
</div>
|
|
387
261
|
);
|
|
388
262
|
}
|
|
389
263
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
264
|
+
const routes = defineRoutes([
|
|
265
|
+
{
|
|
266
|
+
path: "/users",
|
|
267
|
+
component: PageWrapper,
|
|
268
|
+
children: [
|
|
269
|
+
{ path: "/", component: Users },
|
|
270
|
+
{ path: "/:id", component: User }
|
|
271
|
+
]
|
|
272
|
+
}
|
|
273
|
+
]);
|
|
394
274
|
```
|
|
395
275
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
You can nest indefinitely - just remember that only leaf nodes will become their own routes. In this example, the only route created is `/layer1/layer2`, and it appears as three nested divs.
|
|
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>
|
|
412
|
-
```
|
|
276
|
+
You can nest indefinitely. In this example the only route created is `/layer1/layer2`, rendered as three nested divs:
|
|
413
277
|
|
|
414
|
-
|
|
278
|
+
```tsx
|
|
279
|
+
{
|
|
280
|
+
path: "/",
|
|
281
|
+
component: props => <div>Onion starts here {props.children}</div>,
|
|
282
|
+
children: [{
|
|
283
|
+
path: "layer1",
|
|
284
|
+
component: props => <div>Another layer {props.children}</div>,
|
|
285
|
+
children: [{ path: "layer2", component: () => <div>Innermost layer</div> }]
|
|
286
|
+
}]
|
|
287
|
+
}
|
|
288
|
+
```
|
|
415
289
|
|
|
416
|
-
|
|
290
|
+
### Lazy Route Subtrees
|
|
417
291
|
|
|
418
|
-
|
|
292
|
+
`children` also accepts a thunk, so a whole section's route table (not just its components) stays out of the initial bundle:
|
|
419
293
|
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
|
|
294
|
+
```tsx
|
|
295
|
+
// admin/routes.ts
|
|
296
|
+
export default defineRoutes([
|
|
297
|
+
{ path: "/", component: lazy(() => import("./Dashboard")) },
|
|
298
|
+
{ path: "/users/:id", matchFilters: { id: int }, component: lazy(() => import("./User")) }
|
|
299
|
+
]);
|
|
423
300
|
|
|
424
|
-
|
|
301
|
+
// app.ts
|
|
302
|
+
const router = createRouter({
|
|
303
|
+
routes: [
|
|
304
|
+
{ path: "/", component: Home },
|
|
305
|
+
{ path: "/admin", component: AdminShell, children: () => import("./admin/routes") }
|
|
306
|
+
]
|
|
307
|
+
});
|
|
308
|
+
```
|
|
425
309
|
|
|
426
|
-
|
|
427
|
-
function preloadUser({ params, location }) {
|
|
428
|
-
// do preloading
|
|
429
|
-
}
|
|
310
|
+
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:
|
|
430
311
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
312
|
+
- **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.
|
|
313
|
+
- **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.
|
|
314
|
+
- **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.
|
|
315
|
+
- **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.
|
|
434
316
|
|
|
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> |
|
|
317
|
+
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.
|
|
440
318
|
|
|
441
|
-
|
|
319
|
+
## Typed Paths
|
|
442
320
|
|
|
443
|
-
|
|
444
|
-
import { lazy } from "solid-js";
|
|
445
|
-
import { Route } from "@solidjs/router";
|
|
446
|
-
import preloadUser from "./pages/users/[id].data.js";
|
|
447
|
-
const User = lazy(() => import("/pages/users/[id].js"));
|
|
321
|
+
`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:
|
|
448
322
|
|
|
449
|
-
|
|
450
|
-
|
|
323
|
+
```tsx
|
|
324
|
+
paths.users(123) // ok — matchFilters flow into the callsite
|
|
325
|
+
paths.users(2).settings // chainable into children
|
|
326
|
+
paths.users(2, { tab: "x" }, "comments") // "/users/2?tab=x#comments"
|
|
327
|
+
paths.about() // zero-arg/search calls terminate to a plain string
|
|
328
|
+
paths() // "/" — the root
|
|
451
329
|
```
|
|
452
330
|
|
|
453
|
-
|
|
331
|
+
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.
|
|
454
332
|
|
|
455
|
-
##
|
|
333
|
+
## Links
|
|
456
334
|
|
|
457
|
-
|
|
335
|
+
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.
|
|
458
336
|
|
|
459
|
-
|
|
337
|
+
Behavior modifiers are attributes, so they work identically in client, server-rendered, and third-party markup:
|
|
460
338
|
|
|
461
|
-
|
|
339
|
+
| attribute | description |
|
|
340
|
+
| ---------- | ------------------------------------------------------------------------------ |
|
|
341
|
+
| `replace` | Replace the history entry instead of pushing |
|
|
342
|
+
| `noscroll` | Turn off scrolling to the top after navigation |
|
|
343
|
+
| `state` | JSON string [pushed](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) onto the history stack |
|
|
344
|
+
| `preload` | Set to `"false"` to opt this link out of hover/focus preloading |
|
|
345
|
+
| `link` | Marks a router link when `explicitLinks` is enabled |
|
|
346
|
+
| `target` | Any value (e.g. `_self`) opts the anchor out of router handling |
|
|
462
347
|
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
348
|
+
```tsx
|
|
349
|
+
<a href={paths.login} replace>Log in</a>
|
|
350
|
+
<a href={paths.docs} noscroll>Docs</a>
|
|
351
|
+
<a href="https://example.com">External — untouched</a>
|
|
467
352
|
```
|
|
468
353
|
|
|
469
|
-
|
|
354
|
+
Active and pending state is styled with CSS — one vocabulary for every kind of link:
|
|
470
355
|
|
|
471
|
-
|
|
356
|
+
```css
|
|
357
|
+
nav a[aria-current="page"] { font-weight: 600; } /* exact match */
|
|
358
|
+
nav a[data-active] { color: var(--accent); } /* exact or prefix match */
|
|
359
|
+
a[data-pending] { opacity: 0.6; } /* target of in-flight navigation */
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
(The root path only ever matches exactly, so `href={paths()}` doesn't light up on every page.)
|
|
363
|
+
|
|
364
|
+
For component-library links that need reactive state beyond CSS, `useLinkState` is the programmatic counterpart of the attribute vocabulary:
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
import { useLinkState } from "@solidjs/router";
|
|
368
|
+
|
|
369
|
+
function TabLink(props: { href: string; children: JSX.Element }) {
|
|
370
|
+
const link = useLinkState(() => props.href);
|
|
371
|
+
return (
|
|
372
|
+
<a href={props.href} class="tab" data-selected={link.active() || undefined}>
|
|
373
|
+
{props.children}
|
|
374
|
+
</a>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
```
|
|
472
378
|
|
|
473
|
-
|
|
474
|
-
2. It fills a preload cache in the browser which lasts 5 seconds. When a route is preloaded on hover or when preload is called when entering a route it will make sure to dedupe calls.
|
|
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.
|
|
379
|
+
## Preload Functions
|
|
477
380
|
|
|
478
|
-
|
|
381
|
+
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.
|
|
479
382
|
|
|
480
|
-
```
|
|
383
|
+
```tsx
|
|
481
384
|
import { lazy } from "solid-js";
|
|
482
|
-
import { Route } from "@solidjs/router";
|
|
483
|
-
import { getUser } from ... // the query function
|
|
484
385
|
|
|
485
386
|
const User = lazy(() => import("./pages/users/[id].js"));
|
|
486
387
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
void getUser(params.id)
|
|
388
|
+
function preloadUser({ params, location }) {
|
|
389
|
+
void getUser(params.id);
|
|
490
390
|
}
|
|
491
391
|
|
|
492
|
-
|
|
493
|
-
<Route path="/users/:id" component={User} preload={preloadUser} />;
|
|
392
|
+
const routes = defineRoutes([{ path: "/users/:id", component: User, preload: preloadUser }]);
|
|
494
393
|
```
|
|
495
394
|
|
|
496
|
-
|
|
395
|
+
The preload function receives:
|
|
497
396
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
397
|
+
| key | type | description |
|
|
398
|
+
| -------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
399
|
+
| params | object | The route parameters (same value as `useParams()` inside the route component) |
|
|
400
|
+
| location | `{ pathname, search, hash, query, state, key }` | Path information (corresponds to [`useLocation()`](#uselocation)) |
|
|
401
|
+
| 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 |
|
|
502
402
|
|
|
503
|
-
|
|
504
|
-
const user = createMemo(() => getUser(props.params.id));
|
|
505
|
-
return <h1>{user().name}</h1>;
|
|
506
|
-
}
|
|
507
|
-
```
|
|
403
|
+
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`.
|
|
508
404
|
|
|
509
|
-
|
|
405
|
+
## Data APIs
|
|
510
406
|
|
|
511
|
-
|
|
512
|
-
let id = 5;
|
|
407
|
+
These are entirely optional, but they demonstrate the power of the preload mechanism.
|
|
513
408
|
|
|
514
|
-
|
|
515
|
-
getUser.keyFor(id); // returns "users[5]"
|
|
516
|
-
```
|
|
409
|
+
### `query`
|
|
517
410
|
|
|
518
|
-
|
|
411
|
+
Wrap a fetching function to dedupe calls and participate in revalidation:
|
|
412
|
+
|
|
413
|
+
```tsx
|
|
414
|
+
const getUser = query(async id => {
|
|
415
|
+
return (await fetch(`/api/users/${id}`)).json();
|
|
416
|
+
}, "users"); // query key; arguments are serialized alongside it
|
|
417
|
+
```
|
|
519
418
|
|
|
520
|
-
|
|
419
|
+
A query:
|
|
521
420
|
|
|
522
|
-
|
|
421
|
+
1. Dedupes on the server for the lifetime of the request.
|
|
422
|
+
2. Fills a preload cache in the browser lasting 5 seconds, so hover preloads and route entry share one fetch.
|
|
423
|
+
3. Refetches reactively by key on action revalidation.
|
|
424
|
+
4. Serves as a back/forward cache for browser navigation up to 5 minutes; user-initiated navigation bypasses it.
|
|
523
425
|
|
|
524
|
-
|
|
426
|
+
Consume results directly with Solid primitives — there is no router-specific async wrapper:
|
|
525
427
|
|
|
526
|
-
```
|
|
428
|
+
```tsx
|
|
527
429
|
const user = createMemo(() => getUser(params.id));
|
|
528
430
|
return <h1>{user().name}</h1>;
|
|
431
|
+
|
|
432
|
+
// deeply reactive object data
|
|
433
|
+
const todos = createProjection(() => getTodos(), []);
|
|
529
434
|
```
|
|
530
435
|
|
|
531
|
-
|
|
436
|
+
Keys support targeted invalidation:
|
|
532
437
|
|
|
533
|
-
```
|
|
534
|
-
|
|
438
|
+
```ts
|
|
439
|
+
getUser.key; // "users"
|
|
440
|
+
getUser.keyFor(5); // "users[5]"
|
|
535
441
|
```
|
|
536
442
|
|
|
443
|
+
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.
|
|
444
|
+
|
|
537
445
|
### `action`
|
|
538
446
|
|
|
539
|
-
|
|
447
|
+
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:
|
|
540
448
|
|
|
541
|
-
```
|
|
542
|
-
import { action
|
|
449
|
+
```tsx
|
|
450
|
+
import { action } from "@solidjs/router";
|
|
451
|
+
import { redirect } from "@solidjs/web";
|
|
452
|
+
import { paths } from "./router";
|
|
543
453
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
throw redirect("/", { revalidate: getUser.keyFor(data.id) }); // throw a response to do a redirect
|
|
454
|
+
const updateUser = action(async (form: FormData) => {
|
|
455
|
+
await db.users.update(form.get("id"), form);
|
|
456
|
+
throw redirect(paths.users(form.get("id"))); // typed paths work in redirects
|
|
548
457
|
});
|
|
458
|
+
```
|
|
549
459
|
|
|
550
|
-
|
|
551
|
-
<form action={
|
|
460
|
+
```tsx
|
|
461
|
+
<form action={updateUser} method="post">
|
|
462
|
+
<button>Save</button>
|
|
463
|
+
</form>
|
|
552
464
|
|
|
553
|
-
//or
|
|
554
|
-
<button type="submit" formaction={
|
|
465
|
+
// or
|
|
466
|
+
<button type="submit" formaction={updateUser}>Save</button>
|
|
555
467
|
```
|
|
556
468
|
|
|
557
|
-
Actions only work with
|
|
469
|
+
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:
|
|
558
470
|
|
|
559
|
-
|
|
471
|
+
```css
|
|
472
|
+
form[aria-busy] button { pointer-events: none; opacity: 0.6; }
|
|
473
|
+
```
|
|
560
474
|
|
|
561
|
-
|
|
475
|
+
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.
|
|
476
|
+
|
|
477
|
+
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.)
|
|
478
|
+
|
|
479
|
+
For optimistic UI, attach owner-scoped hooks to the action and use Solid's optimistic primitives for rendered state:
|
|
480
|
+
|
|
481
|
+
```tsx
|
|
562
482
|
import { createOptimisticStore } from "solid-js";
|
|
563
483
|
import { action, query } from "@solidjs/router";
|
|
564
484
|
|
|
565
485
|
const getTodos = query(async () => fetchTodos(), "todos");
|
|
566
486
|
const [todos, setTodos] = createOptimisticStore(() => getTodos(), []);
|
|
567
487
|
|
|
568
|
-
const addTodo = action(async
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
});
|
|
488
|
+
const addTodo = action(async todo => {
|
|
489
|
+
await saveTodo(todo);
|
|
490
|
+
return { ok: true, todo };
|
|
491
|
+
}, "add-todo").onSubmit(todo => {
|
|
492
|
+
setTodos(items => {
|
|
493
|
+
items.push({ ...todo, pending: true });
|
|
575
494
|
});
|
|
495
|
+
});
|
|
576
496
|
```
|
|
577
497
|
|
|
578
|
-
`
|
|
579
|
-
|
|
580
|
-
The preferred pattern is for actions to return values and let the client interpret the result. Throwing errors is still supported, but `Submission.error` is mainly an escape hatch for that legacy style.
|
|
581
|
-
|
|
582
|
-
Sometimes it might be easier to deal with typed data instead of `FormData` and adding additional hidden fields. For that reason Actions have a with method. That works similar to `bind` which applies the arguments in order.
|
|
583
|
-
|
|
584
|
-
Picture an action that deletes Todo Item:
|
|
498
|
+
`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.
|
|
585
499
|
|
|
586
|
-
|
|
587
|
-
const deleteTodo = action(async (formData: FormData) => {
|
|
588
|
-
const id = Number(formData.get("id"))
|
|
589
|
-
await api.deleteTodo(id)
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
<form action={deleteTodo} method="post">
|
|
593
|
-
<input type="hidden" name="id" value={todo.id} />
|
|
594
|
-
<button type="submit">Delete</button>
|
|
595
|
-
</form>
|
|
596
|
-
```
|
|
500
|
+
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.
|
|
597
501
|
|
|
598
|
-
|
|
502
|
+
Actions have a `with` method (like `bind`) for typed arguments instead of hidden form fields:
|
|
599
503
|
|
|
600
|
-
```
|
|
601
|
-
const deleteTodo = action(api.deleteTodo)
|
|
504
|
+
```tsx
|
|
505
|
+
const deleteTodo = action(api.deleteTodo);
|
|
602
506
|
|
|
603
507
|
<form action={deleteTodo.with(todo.id)} method="post">
|
|
604
508
|
<button type="submit">Delete</button>
|
|
605
|
-
</form
|
|
509
|
+
</form>;
|
|
606
510
|
```
|
|
607
511
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
#### Notes on `<form>` implementation and SSR
|
|
611
|
-
|
|
612
|
-
This requires stable references as you can only serialize a string as an attribute, and across SSR they'd need to match. The solution is providing a unique name.
|
|
613
|
-
|
|
614
|
-
```jsx
|
|
615
|
-
const myAction = action(async (args) => {}, "my-action");
|
|
616
|
-
```
|
|
512
|
+
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")`.
|
|
617
513
|
|
|
618
514
|
### `useAction`
|
|
619
515
|
|
|
620
|
-
|
|
516
|
+
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:
|
|
621
517
|
|
|
622
|
-
```
|
|
623
|
-
// in component
|
|
518
|
+
```tsx
|
|
624
519
|
const submit = useAction(myAction);
|
|
625
520
|
submit(...args);
|
|
626
521
|
```
|
|
627
522
|
|
|
628
|
-
The outside of a form context you can use custom data instead of formData, and these helpers preserve types. However, even when used with server functions (in projects like SolidStart) this requires client side javascript and is not Progressive Enhanceable like forms are.
|
|
629
|
-
|
|
630
523
|
### `useSubmissions`
|
|
631
524
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
```jsx
|
|
635
|
-
type Submission<T, U> = {
|
|
636
|
-
readonly input: T;
|
|
637
|
-
readonly result?: U;
|
|
638
|
-
readonly error: any;
|
|
639
|
-
readonly url: string;
|
|
640
|
-
clear: () => void;
|
|
641
|
-
retry: () => void;
|
|
642
|
-
};
|
|
525
|
+
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:
|
|
643
526
|
|
|
644
|
-
|
|
645
|
-
const
|
|
527
|
+
```tsx
|
|
528
|
+
const submissions = useSubmissions(action, input => filter(input));
|
|
529
|
+
const latest = submissions.at(-1);
|
|
530
|
+
// { input, result?, error, url, clear(), retry() }
|
|
646
531
|
```
|
|
647
532
|
|
|
648
|
-
Use Solid's `createOptimistic`
|
|
533
|
+
Use Solid's `createOptimistic` / `createOptimisticStore` for in-flight UI.
|
|
649
534
|
|
|
650
|
-
|
|
535
|
+
## Typed Search Params
|
|
651
536
|
|
|
652
|
-
|
|
537
|
+
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`:
|
|
653
538
|
|
|
654
|
-
|
|
539
|
+
```tsx
|
|
540
|
+
import * as v from "valibot";
|
|
655
541
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
})
|
|
542
|
+
const routes = defineRoutes([
|
|
543
|
+
{
|
|
544
|
+
path: "/search",
|
|
545
|
+
component: Search,
|
|
546
|
+
search: v.object({
|
|
547
|
+
q: v.optional(v.string(), ""),
|
|
548
|
+
page: v.optional(v.pipe(v.unknown(), v.transform(Number)), 1)
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
]);
|
|
664
552
|
```
|
|
665
553
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
554
|
+
```tsx
|
|
555
|
+
const [search, setSearch] = useSearchParams(paths.search);
|
|
556
|
+
search.page; // number (parsed, not "2")
|
|
557
|
+
setSearch({ page: search.page + 1 }); // typed setter
|
|
669
558
|
|
|
670
|
-
|
|
671
|
-
const getTodo = query(async (id: number) => {
|
|
672
|
-
const todo = await fetchTodo(id);
|
|
673
|
-
return todo;
|
|
674
|
-
}, "todo");
|
|
675
|
-
|
|
676
|
-
const updateTodo = action(async (todo: Todo) => {
|
|
677
|
-
await updateTodo(todo.id, todo);
|
|
678
|
-
reload({ revalidate: getTodo.keyFor(todo.id) });
|
|
679
|
-
});
|
|
559
|
+
<a href={paths.search({ q: "solid", page: 2 })}>Search</a>; // typed builder
|
|
680
560
|
```
|
|
681
561
|
|
|
682
|
-
|
|
562
|
+
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.
|
|
683
563
|
|
|
684
|
-
|
|
564
|
+
## Router Config Reference
|
|
685
565
|
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
import { render } from "@solidjs/web";
|
|
689
|
-
import { Router } from "@solidjs/router";
|
|
690
|
-
|
|
691
|
-
const routes = [
|
|
692
|
-
{
|
|
693
|
-
path: "/users",
|
|
694
|
-
component: lazy(() => import("/pages/users.js")),
|
|
695
|
-
},
|
|
696
|
-
{
|
|
697
|
-
path: "/users/:id",
|
|
698
|
-
component: lazy(() => import("/pages/users/[id].js")),
|
|
699
|
-
children: [
|
|
700
|
-
{
|
|
701
|
-
path: "/",
|
|
702
|
-
component: lazy(() => import("/pages/users/[id]/index.js")),
|
|
703
|
-
},
|
|
704
|
-
{
|
|
705
|
-
path: "/settings",
|
|
706
|
-
component: lazy(() => import("/pages/users/[id]/settings.js")),
|
|
707
|
-
},
|
|
708
|
-
{
|
|
709
|
-
path: "/*all",
|
|
710
|
-
component: lazy(() => import("/pages/users/[id]/[...all].js")),
|
|
711
|
-
},
|
|
712
|
-
],
|
|
713
|
-
},
|
|
714
|
-
{
|
|
715
|
-
path: "/",
|
|
716
|
-
component: lazy(() => import("/pages/index.js")),
|
|
717
|
-
},
|
|
718
|
-
{
|
|
719
|
-
path: "/*all",
|
|
720
|
-
component: lazy(() => import("/pages/[...all].js")),
|
|
721
|
-
},
|
|
722
|
-
];
|
|
723
|
-
|
|
724
|
-
render(() => <Router>{routes}</Router>, document.getElementById("app"));
|
|
566
|
+
```tsx
|
|
567
|
+
createRouter(config);
|
|
725
568
|
```
|
|
726
569
|
|
|
727
|
-
|
|
570
|
+
| option | type | description |
|
|
571
|
+
| --------------- | -------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
572
|
+
| `routes` | `RouteDefinition[]` | The route tree — inline arrays infer literally; wrap extracted trees in `defineRoutes` |
|
|
573
|
+
| `base` | `string` | Base url to use for matching routes |
|
|
574
|
+
| `preload` | `RoutePreloadFunc` | App-wide preload: once per mount/request, result reaches the root render-prop as `props.data` |
|
|
575
|
+
| `history` | `RouterHistory` | History adapter; defaults to browser history on the client and the request URL on the server |
|
|
576
|
+
| `singleFlight` | `boolean` | Single-flight mutations, default `true` |
|
|
577
|
+
| `actionBase` | `string` | Root url for server actions, default `/_server` |
|
|
578
|
+
| `preloadLinks` | `boolean` | Preload route code/data on link hover and focus, default `true` |
|
|
579
|
+
| `explicitLinks` | `boolean` | Require the `link` attribute for router handling instead of intercepting all anchors, default `false` |
|
|
580
|
+
| `transformUrl` | `(url: string) => string` | Rewrite URLs before matching |
|
|
728
581
|
|
|
729
|
-
|
|
730
|
-
import { lazy } from "solid-js";
|
|
731
|
-
import { render } from "@solidjs/web";
|
|
732
|
-
import { Router } from "@solidjs/router";
|
|
733
|
-
|
|
734
|
-
const route = {
|
|
735
|
-
path: "/",
|
|
736
|
-
component: lazy(() => import("/pages/index.js")),
|
|
737
|
-
};
|
|
582
|
+
The returned instance is the provider component and carries the static surface:
|
|
738
583
|
|
|
739
|
-
|
|
740
|
-
|
|
584
|
+
| member | description |
|
|
585
|
+
| --------- | ------------------------------------------------------------------------------------------------ |
|
|
586
|
+
| `paths` | The [typed path proxy](#typed-paths) |
|
|
587
|
+
| `match` | Pure matching against an arbitrary URL — no rendering or request context; root→leaf, `[]` if none |
|
|
588
|
+
| `routes` | The config tree |
|
|
589
|
+
| `config` | The full config — lets server integrations consume the instance directly |
|
|
741
590
|
|
|
742
|
-
##
|
|
591
|
+
## Router Primitives
|
|
743
592
|
|
|
744
|
-
|
|
593
|
+
Hooks read the live session off router context.
|
|
745
594
|
|
|
746
|
-
|
|
595
|
+
### useParams
|
|
747
596
|
|
|
748
|
-
|
|
749
|
-
import { HashRouter } from "@solidjs/router";
|
|
597
|
+
Retrieves a reactive, store-like object of the current route's path parameters. Pass a paths node for typing:
|
|
750
598
|
|
|
751
|
-
|
|
599
|
+
```tsx
|
|
600
|
+
const params = useParams(); // Params (strings)
|
|
601
|
+
const params = useParams(paths.users); // { id: number } — typed via matchFilters
|
|
752
602
|
```
|
|
753
603
|
|
|
754
|
-
###
|
|
604
|
+
### useNavigate
|
|
755
605
|
|
|
756
|
-
|
|
606
|
+
Retrieves a method to navigate. Accepts a string or a typed path node, plus options:
|
|
757
607
|
|
|
758
|
-
|
|
759
|
-
|
|
608
|
+
- `resolve` (_boolean_, default `true`): resolve the path against the current route
|
|
609
|
+
- `replace` (_boolean_, default `false`): replace the history entry
|
|
610
|
+
- `scroll` (_boolean_, default `true`): scroll to top after navigation
|
|
611
|
+
- `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))
|
|
760
612
|
|
|
761
|
-
|
|
613
|
+
```tsx
|
|
614
|
+
const navigate = useNavigate();
|
|
615
|
+
navigate(paths.login, { replace: true });
|
|
762
616
|
```
|
|
763
617
|
|
|
764
|
-
|
|
618
|
+
For declarative redirects on render (the old `<Navigate>`), call it during component setup or redirect from a preload.
|
|
765
619
|
|
|
766
|
-
|
|
620
|
+
### useLocation
|
|
767
621
|
|
|
768
|
-
|
|
769
|
-
import { isServer } from "@solidjs/web";
|
|
770
|
-
import { Router } from "@solidjs/router";
|
|
622
|
+
Retrieves the reactive `location` object:
|
|
771
623
|
|
|
772
|
-
|
|
624
|
+
```tsx
|
|
625
|
+
const location = useLocation();
|
|
626
|
+
const pathname = createMemo(() => parsePath(location.pathname));
|
|
773
627
|
```
|
|
774
628
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
### `<Router>`
|
|
629
|
+
### useSearchParams
|
|
778
630
|
|
|
779
|
-
|
|
631
|
+
See [Typed Search Params](#typed-search-params). Reads are proxied — access properties to subscribe.
|
|
780
632
|
|
|
781
|
-
|
|
782
|
-
| ------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
783
|
-
| children | `JSX.Element`, `RouteDefinition`, or `RouteDefinition[]` | The route definitions |
|
|
784
|
-
| root | Component | Top level layout component |
|
|
785
|
-
| base | string | Base url to use for matching routes |
|
|
786
|
-
| actionBase | string | Root url for server actions, default: `/_server` |
|
|
787
|
-
| preload | boolean | Enables/disables preloads globally, default: `true` |
|
|
788
|
-
| 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">`.) |
|
|
633
|
+
### useIsRouting
|
|
789
634
|
|
|
790
|
-
|
|
635
|
+
A signal indicating whether the router is processing a navigation — useful for pending UI while the next route and its data settle:
|
|
791
636
|
|
|
792
|
-
|
|
637
|
+
```tsx
|
|
638
|
+
const isRouting = useIsRouting();
|
|
639
|
+
return <div classList={{ "grey-out": isRouting() }}>...</div>;
|
|
640
|
+
```
|
|
793
641
|
|
|
794
|
-
|
|
642
|
+
### useMatch
|
|
795
643
|
|
|
796
|
-
|
|
797
|
-
| ------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
798
|
-
| href | string | The path of the route to navigate to. This will be resolved relative to the route that the link is in, but you can preface it with `/` to refer back to the root. |
|
|
799
|
-
| noScroll | boolean | If true, turn off the default behavior of scrolling to the top of the new page |
|
|
800
|
-
| 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.) |
|
|
801
|
-
| state | unknown | [Push this value](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) to the history stack when navigating |
|
|
802
|
-
| inactiveClass | string | The class to show when the link is inactive (when the current location doesn't match the link) |
|
|
803
|
-
| activeClass | string | The class to show when the link is active |
|
|
804
|
-
| 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` |
|
|
644
|
+
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:
|
|
805
645
|
|
|
806
|
-
|
|
646
|
+
```tsx
|
|
647
|
+
const match = useMatch(() => "/admin/*rest");
|
|
648
|
+
return <Show when={match()}>...</Show>;
|
|
649
|
+
```
|
|
807
650
|
|
|
808
|
-
|
|
651
|
+
### useRouteMatches
|
|
809
652
|
|
|
810
|
-
|
|
811
|
-
function getPath({ navigate, location }) {
|
|
812
|
-
// navigate is the result of calling useNavigate(); location is the result of calling useLocation().
|
|
813
|
-
// You can use those to dynamically determine a path to navigate to
|
|
814
|
-
return "/some-path";
|
|
815
|
-
}
|
|
653
|
+
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:
|
|
816
654
|
|
|
817
|
-
|
|
818
|
-
|
|
655
|
+
```tsx
|
|
656
|
+
const matches = useRouteMatches();
|
|
657
|
+
const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb));
|
|
819
658
|
```
|
|
820
659
|
|
|
821
|
-
###
|
|
660
|
+
### usePreloadRoute
|
|
822
661
|
|
|
823
|
-
|
|
662
|
+
Returns a function to preload a route manually — the same work link hover/focus triggers automatically. Accepts strings, URLs, and typed path nodes:
|
|
824
663
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
| matchFilters | `MatchFilters` | Additional constraints for matching against the route |
|
|
830
|
-
| children | `JSX.Element` | Nested `<Route>` definitions |
|
|
831
|
-
| preload | `RoutePreloadFunc` | Function called during preload or when the route is navigated to. |
|
|
664
|
+
```tsx
|
|
665
|
+
const preload = usePreloadRoute();
|
|
666
|
+
preload(paths.users(2).settings, { preloadData: true });
|
|
667
|
+
```
|
|
832
668
|
|
|
833
|
-
|
|
669
|
+
### useLinkState
|
|
834
670
|
|
|
835
|
-
|
|
671
|
+
Reactive `active`/`current`/`pending` state for [custom link components](#links).
|
|
836
672
|
|
|
837
|
-
###
|
|
673
|
+
### useBeforeLeave
|
|
838
674
|
|
|
839
|
-
|
|
675
|
+
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:
|
|
840
676
|
|
|
841
|
-
|
|
842
|
-
|
|
677
|
+
- `from` (_Location_): current location (before change)
|
|
678
|
+
- `to` (_string | number_): path passed to `navigate`
|
|
679
|
+
- `options` (_NavigateOptions_): options passed to `navigate`
|
|
680
|
+
- `preventDefault()`: call to block the route change
|
|
681
|
+
- `defaultPrevented` (_readonly boolean_): `true` if any previous handler called `preventDefault`
|
|
682
|
+
- `retry(force?)`: retry the navigation, e.g. after confirming with the user; pass `true` to skip re-running leave handlers
|
|
843
683
|
|
|
844
|
-
|
|
845
|
-
|
|
684
|
+
```tsx
|
|
685
|
+
useBeforeLeave((e: BeforeLeaveEventArgs) => {
|
|
686
|
+
if (form.isDirty && !e.defaultPrevented) {
|
|
687
|
+
e.preventDefault();
|
|
688
|
+
setTimeout(() => {
|
|
689
|
+
if (window.confirm("Discard unsaved changes - are you sure?")) {
|
|
690
|
+
e.retry(true);
|
|
691
|
+
}
|
|
692
|
+
}, 100);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
846
695
|
```
|
|
847
696
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
Retrieves method to do navigation. The method accepts a path to navigate to and an optional object with the following options:
|
|
697
|
+
## Other Environments
|
|
851
698
|
|
|
852
|
-
|
|
853
|
-
- replace (_boolean_, default `false`): replace the history entry
|
|
854
|
-
- scroll (_boolean_, default `true`): scroll to top after navigation
|
|
855
|
-
- state (_any_, default `undefined`): pass custom state to `location.state`
|
|
699
|
+
History adapters are plain imports, so unused ones never enter your bundle:
|
|
856
700
|
|
|
857
|
-
|
|
701
|
+
```tsx
|
|
702
|
+
import { createRouter, hashHistory, memoryHistory } from "@solidjs/router";
|
|
858
703
|
|
|
859
|
-
|
|
860
|
-
const
|
|
704
|
+
// hash mode
|
|
705
|
+
const Router = createRouter({ routes, history: hashHistory() });
|
|
861
706
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}
|
|
707
|
+
// tests and non-browser environments
|
|
708
|
+
const Router = createRouter({ routes, history: memoryHistory("/users/1") });
|
|
865
709
|
```
|
|
866
710
|
|
|
867
|
-
###
|
|
868
|
-
|
|
869
|
-
Retrieves reactive `location` object useful for getting things like `pathname`.
|
|
711
|
+
### Environments without Proxy
|
|
870
712
|
|
|
871
|
-
|
|
872
|
-
const location = useLocation();
|
|
713
|
+
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:
|
|
873
714
|
|
|
874
|
-
|
|
715
|
+
```tsx
|
|
716
|
+
const base = browserHistory();
|
|
717
|
+
const history = {
|
|
718
|
+
...base,
|
|
719
|
+
// wrappers build objects with defined getters instead of a Proxy
|
|
720
|
+
utils: { ...base.utils, paramsWrapper, queryWrapper }
|
|
721
|
+
};
|
|
722
|
+
const Router = createRouter({ routes, history });
|
|
875
723
|
```
|
|
876
724
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them. The object is a proxy so you must access properties to subscribe to reactive updates. Note values will be strings and property names will retain their casing.
|
|
725
|
+
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.
|
|
880
726
|
|
|
881
|
-
The
|
|
727
|
+
The instance also matches arbitrary URLs anywhere — server middleware, sitemap generation, tests — with no rendering involved:
|
|
882
728
|
|
|
883
|
-
```
|
|
884
|
-
|
|
729
|
+
```tsx
|
|
730
|
+
import { Router } from "./router";
|
|
885
731
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
setSearchParams({ page: (parseInt(searchParams.page) || 0) + 1 })
|
|
892
|
-
}
|
|
893
|
-
>
|
|
894
|
-
Next Page
|
|
895
|
-
</button>
|
|
896
|
-
</div>
|
|
897
|
-
);
|
|
732
|
+
Router.match("/users/2/settings?tab=x");
|
|
733
|
+
// [
|
|
734
|
+
// { path: "/users/:id", match: "/users/2", params: { id: "2" } },
|
|
735
|
+
// { path: "/settings", match: "/users/2/settings", params: {} }
|
|
736
|
+
// ]
|
|
898
737
|
```
|
|
899
738
|
|
|
900
|
-
|
|
739
|
+
## Server Integration
|
|
901
740
|
|
|
902
|
-
|
|
741
|
+
Framework handler wiring lives in `@solidjs/router/server`. Both integrations accept the router instance directly — its routes, base, and preload are the single source of truth:
|
|
903
742
|
|
|
904
|
-
```
|
|
905
|
-
|
|
743
|
+
```tsx
|
|
744
|
+
import { createFlightDataCollector, createNoJSHandler } from "@solidjs/router/server";
|
|
745
|
+
import { Router } from "./app/router";
|
|
906
746
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
<MyAwesomeContent />
|
|
910
|
-
</div>
|
|
911
|
-
);
|
|
747
|
+
const collectFlightData = createFlightDataCollector(Router);
|
|
748
|
+
const handleNoJS = createNoJSHandler();
|
|
912
749
|
```
|
|
913
750
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
`useMatch` takes an accessor that returns the path and creates a Memo that returns match information if the current path matches the provided path. Useful for determining if a given path matches the current route.
|
|
751
|
+
`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. `createNoJSHandler` implements the no-JS form convention: form posts without the client runtime redirect back with the outcome in a one-shot flash cookie that SSR reads into submission state. Both policies previously lived inside SolidStart; the router now owns them, so custom server setups get single-flight mutations and progressive enhancement without a framework.
|
|
917
752
|
|
|
918
|
-
|
|
919
|
-
const match = useMatch(() => props.href);
|
|
920
|
-
|
|
921
|
-
return <div class={{ active: Boolean(match()) }} />;
|
|
922
|
-
```
|
|
753
|
+
## Migration from 0.x
|
|
923
754
|
|
|
924
|
-
|
|
755
|
+
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.
|
|
925
756
|
|
|
926
|
-
|
|
757
|
+
### Router components → `createRouter`
|
|
927
758
|
|
|
928
|
-
|
|
759
|
+
```tsx
|
|
760
|
+
// 0.x
|
|
761
|
+
<Router root={App}>
|
|
762
|
+
<Route path="/users" component={Users} />
|
|
763
|
+
<Route path="/users/:id" component={User} />
|
|
764
|
+
</Router>
|
|
929
765
|
|
|
930
|
-
|
|
931
|
-
const
|
|
766
|
+
// 1.0
|
|
767
|
+
const Router = createRouter({
|
|
768
|
+
routes: [
|
|
769
|
+
{ path: "/users", component: Users },
|
|
770
|
+
{ path: "/users/:id", component: User }
|
|
771
|
+
]
|
|
772
|
+
});
|
|
932
773
|
|
|
933
|
-
|
|
934
|
-
matches().map((m) => m.route.info.breadcrumb)
|
|
935
|
-
);
|
|
774
|
+
<Router>{props => <App {...props} />}</Router>
|
|
936
775
|
```
|
|
937
776
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
```js
|
|
943
|
-
const preload = usePreloadRoute();
|
|
944
|
-
|
|
945
|
-
preload(`/users/settings`, { preloadData: true });
|
|
946
|
-
```
|
|
777
|
+
- `<HashRouter>` → `createRouter({ routes, history: hashHistory() })`
|
|
778
|
+
- `<MemoryRouter>` / `createMemoryHistory` → `createRouter({ routes, history: memoryHistory("/initial") })`
|
|
779
|
+
- `<StaticRouter url>` / `<Router url>` for SSR → automatic from the request URL; without a request event, pass `memoryHistory(url)`
|
|
780
|
+
- `root` prop → the render-prop child; `rootPreload` → the factory's `preload` option
|
|
947
781
|
|
|
948
|
-
###
|
|
782
|
+
### JSX `<Route>` → config objects
|
|
949
783
|
|
|
950
|
-
`
|
|
784
|
+
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.
|
|
951
785
|
|
|
952
|
-
|
|
953
|
-
- to (_string | number_): path passed to `navigate`.
|
|
954
|
-
- options (_NavigateOptions_): options passed to `navigate`.
|
|
955
|
-
- preventDefault (_function_): call to block the route change.
|
|
956
|
-
- defaultPrevented (_readonly boolean_): `true` if any previously called leave handlers called `preventDefault`.
|
|
957
|
-
- 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).
|
|
786
|
+
### `<A>` → plain `<a>`
|
|
958
787
|
|
|
959
|
-
|
|
788
|
+
- `<A href replace noScroll state>` → `<a href replace noscroll state>` (attributes, all lowercase)
|
|
789
|
+
- `activeClass` / `inactiveClass` → CSS attribute selectors on `[data-active]` / `[aria-current="page"]`
|
|
790
|
+
- `end` → style exact matches with `[aria-current="page"]` instead of `[data-active]`; the root path already only matches exactly
|
|
791
|
+
- Route-relative hrefs → typed `paths`; `useResolvedPath` / `useHref` remain for manual resolution
|
|
792
|
+
- Custom link components → `useLinkState`
|
|
960
793
|
|
|
961
|
-
|
|
962
|
-
useBeforeLeave((e: BeforeLeaveEventArgs) => {
|
|
963
|
-
if (form.isDirty && !e.defaultPrevented) {
|
|
964
|
-
// preventDefault to block immediately and prompt user async
|
|
965
|
-
e.preventDefault();
|
|
966
|
-
setTimeout(() => {
|
|
967
|
-
if (window.confirm("Discard unsaved changes - are you sure?")) {
|
|
968
|
-
// user wants to proceed anyway so retry with force=true
|
|
969
|
-
e.retry(true);
|
|
970
|
-
}
|
|
971
|
-
}, 100);
|
|
972
|
-
}
|
|
973
|
-
});
|
|
974
|
-
```
|
|
794
|
+
### Removed and renamed
|
|
975
795
|
|
|
976
|
-
|
|
796
|
+
- `<Navigate>` → call `useNavigate()` during component setup, or redirect from a preload
|
|
797
|
+
- `useCurrentMatches` → `useRouteMatches` (same behavior)
|
|
798
|
+
- `redirect` / `reload` → import from `@solidjs/web`; they're protocol-level and work without the router
|
|
799
|
+
- `json(data, init)` → `respond(data, init)` from `@solidjs/web`
|
|
800
|
+
- `cache` (deprecated alias) → `query`
|
|
977
801
|
|
|
978
|
-
|
|
802
|
+
### Data APIs (Solid 2)
|
|
979
803
|
|
|
980
|
-
|
|
804
|
+
- `createAsync` / `createAsyncStore` are gone — read `query()` results with Solid 2 primitives: `createMemo`, `createProjection`, `createOptimistic`, `createOptimisticStore`.
|
|
981
805
|
|
|
982
|
-
|
|
806
|
+
```tsx
|
|
807
|
+
// 0.x
|
|
808
|
+
const user = createAsync(() => getUser(params.id));
|
|
983
809
|
|
|
984
|
-
|
|
810
|
+
// 1.0
|
|
985
811
|
const user = createMemo(() => getUser(params.id));
|
|
986
|
-
const [todos, setTodos] = createOptimisticStore(() => getTodos(), []);
|
|
987
812
|
```
|
|
988
813
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
Continue using `query()` for cached reads and invalidation, but consume the results directly through Solid 2's async primitives instead of router-specific wrappers.
|
|
992
|
-
|
|
993
|
-
### `action()` lifecycle hooks changed
|
|
814
|
+
- `query()` stays the source of truth for cached reads and invalidation.
|
|
815
|
+
- `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)`.
|
|
994
816
|
|
|
995
|
-
|
|
817
|
+
```tsx
|
|
818
|
+
// 0.x — read in-flight state off the submission
|
|
819
|
+
const submitting = useSubmission(addTodo);
|
|
820
|
+
<span>{submitting.pending && "Saving..."}</span>;
|
|
996
821
|
|
|
997
|
-
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
.onSubmit(todo => {
|
|
1003
|
-
// optimistic write
|
|
822
|
+
// 1.0 — optimistic primitives own in-flight state
|
|
823
|
+
const [todos, setTodos] = createOptimisticStore(() => getTodos(), []);
|
|
824
|
+
const addTodo = action(saveTodo).onSubmit(todo =>
|
|
825
|
+
setTodos(items => {
|
|
826
|
+
items.push({ ...todo, pending: true });
|
|
1004
827
|
})
|
|
1005
|
-
|
|
1006
|
-
// observe settled result or retry state
|
|
1007
|
-
});
|
|
828
|
+
);
|
|
1008
829
|
```
|
|
1009
830
|
|
|
1010
|
-
-
|
|
1011
|
-
- Use `onSettled(...)` for owner-scoped observation of completed submissions.
|
|
1012
|
-
- Use returned values for expected application-level results. Thrown errors are still captured on `Submission.error` when something fails unexpectedly.
|
|
1013
|
-
|
|
1014
|
-
### `useSubmissions()` is the submission API
|
|
1015
|
-
|
|
1016
|
-
Submissions are now settled history, not in-flight mutation state. Read them through `useSubmissions()` and select the latest entry with `at(-1)` when needed.
|
|
1017
|
-
|
|
1018
|
-
```jsx
|
|
1019
|
-
const submissions = useSubmissions(saveTodo);
|
|
1020
|
-
const latestSubmission = submissions.at(-1);
|
|
1021
|
-
```
|
|
831
|
+
- 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`.
|
|
1022
832
|
|
|
1023
833
|
## SPAs in Deployed Environments
|
|
1024
834
|
|
|
1025
|
-
When deploying
|
|
835
|
+
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.
|
|
1026
836
|
|
|
1027
|
-
|
|
837
|
+
On Netlify, create a `_redirects` file:
|
|
1028
838
|
|
|
1029
839
|
```sh
|
|
1030
840
|
/* /index.html 200
|
|
1031
841
|
```
|
|
1032
842
|
|
|
1033
|
-
On Vercel
|
|
843
|
+
On Vercel, add a rewrites section to `vercel.json`:
|
|
1034
844
|
|
|
1035
845
|
```json
|
|
1036
846
|
{
|