@solidjs/router 0.17.0-next.6 → 1.0.0-next.7

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.
Files changed (48) hide show
  1. package/README.md +526 -716
  2. package/dist/claims.d.ts +21 -0
  3. package/dist/claims.js +108 -0
  4. package/dist/data/action.d.ts +15 -0
  5. package/dist/data/action.js +127 -12
  6. package/dist/data/events.d.ts +8 -0
  7. package/dist/data/events.js +23 -22
  8. package/dist/data/flash.d.ts +1 -5
  9. package/dist/data/flash.js +10 -18
  10. package/dist/data/flashCookie.d.ts +7 -0
  11. package/dist/data/flashCookie.js +20 -0
  12. package/dist/data/serverForms.d.ts +1 -0
  13. package/dist/data/serverForms.js +5 -0
  14. package/dist/index.d.ts +5 -3
  15. package/dist/index.js +1728 -1158
  16. package/dist/index.jsx +2 -2
  17. package/dist/lifecycle.d.ts +29 -4
  18. package/dist/lifecycle.js +40 -37
  19. package/dist/paths.d.ts +117 -0
  20. package/dist/paths.js +41 -0
  21. package/dist/routers/components.d.ts +10 -21
  22. package/dist/routers/components.jsx +29 -51
  23. package/dist/routers/factory.d.ts +45 -0
  24. package/dist/routers/factory.jsx +143 -0
  25. package/dist/routers/history.d.ts +24 -0
  26. package/dist/routers/history.js +180 -0
  27. package/dist/routers/index.d.ts +4 -11
  28. package/dist/routers/index.js +2 -6
  29. package/dist/routing.d.ts +79 -52
  30. package/dist/routing.js +296 -127
  31. package/dist/server.d.ts +25 -15
  32. package/dist/server.js +43 -7
  33. package/dist/types.d.ts +74 -5
  34. package/dist/utils.d.ts +2 -0
  35. package/dist/utils.js +2 -0
  36. package/package.json +6 -6
  37. package/dist/components.d.ts +0 -31
  38. package/dist/components.jsx +0 -46
  39. package/dist/routers/HashRouter.d.ts +0 -9
  40. package/dist/routers/HashRouter.js +0 -41
  41. package/dist/routers/MemoryRouter.d.ts +0 -24
  42. package/dist/routers/MemoryRouter.js +0 -57
  43. package/dist/routers/Router.d.ts +0 -9
  44. package/dist/routers/Router.js +0 -45
  45. package/dist/routers/StaticRouter.d.ts +0 -6
  46. package/dist/routers/StaticRouter.js +0 -15
  47. package/dist/routers/createRouter.d.ts +0 -10
  48. 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, enabling your single-page application to become multi-paged without full page reloads. Fully integrated into the SolidJS ecosystem, Solid Router provides declarative syntax with features like universal rendering and parallel data fetching for best performance.
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
- - **All Routing Modes**:
20
- - [History-Based](https://docs.solidjs.com/solid-router/reference/components/router#router) for standard browser navigation
21
- - [Hash-Based](https://docs.solidjs.com/solid-router/reference/components/hash-router#hashrouter) for navigation based on URL hash
22
- - [Static Routing](https://docs.solidjs.com/solid-router/rendering-modes/ssr#server-side-rendering) for server-side rendering (_SSR_)
23
- - [Memory-Based](https://docs.solidjs.com/solid-router/reference/components/memory-router#memoryrouter) for testing in non-browser environments
24
- - **TypeScript**: Full integration for robust, type-safe development
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 contents
26
+ ## Table of Contents
32
27
 
33
28
  - [Getting Started](#getting-started)
34
- - [Set Up the Router](#set-up-the-router)
35
- - [Configure Your Routes](#configure-your-routes)
36
- - [Create Links to Your Routes](#create-links-to-your-routes)
37
- - [Dynamic Routes](#dynamic-routes)
38
- - [Nested Routes](#nested-routes)
39
- - [Hash Mode Router](#hash-mode-router)
40
- - [Memory Mode Router](#memory-mode-router)
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
- - [Config Based Routing](#config-based-routing)
43
- - [Components](#components)
42
+ - [Typed Search Params](#typed-search-params)
43
+ - [Router Config Reference](#router-config-reference)
44
44
  - [Router Primitives](#router-primitives)
45
- - [useParams](#useparams)
46
- - [useNavigate](#usenavigate)
47
- - [useLocation](#uselocation)
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
- Install `@solidjs/router`, then start your application by rendering the router component
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
- This sets up a Router that will match on the url to display the desired page
59
+ ```tsx
60
+ // app/router.ts
61
+ import { lazy } from "solid-js";
62
+ import { createRouter } from "@solidjs/router";
74
63
 
75
- ### Configure Your Routes
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
- Solid Router allows you to configure your routes using JSX:
80
+ export const { paths } = Router;
81
+ ```
78
82
 
79
- 1. Add each route to a `<Router>` using the `Route` component, specifying a path and a component to render when the user navigates to that path.
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
- ```jsx
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
- import Home from "./pages/Home";
86
- import Users from "./pages/Users";
87
+ ```tsx
88
+ // features/admin/routes.ts
89
+ export const adminRoutes = defineRoutes([
90
+ { path: "/admin", component: Admin, children: [/* ... */] }
91
+ ]);
87
92
 
88
- render(
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
- 2. Provide a root level layout
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
- This will always be there and won't update on page change. It is the ideal place to put top level navigation and Context Providers
102
-
103
- ```jsx
99
+ ```tsx
100
+ // app/index.tsx
104
101
  import { render } from "@solidjs/web";
105
- import { Router, Route } from "@solidjs/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 root={App}>
120
- <Route path="/users" component={Users} />
121
- <Route path="/" component={Home} />
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
- 3. Create a catch-all route (404 page)
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
- const App = (props) => (
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
- render(
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
- );
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
- 4. Lazy-load route components
131
+ ## The Mental Model: Instance vs Hooks
160
132
 
161
- This way, the `Users` and `Home` components will only be loaded if you're navigating to `/users` or `/`, respectively.
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
- ```jsx
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
- const Users = lazy(() => import("./pages/Users"));
169
- const Home = lazy(() => import("./pages/Home"));
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
- const App = (props) => (
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
- render(
179
- () => (
180
- <Router root={App}>
181
- <Route path="/users" component={Users} />
182
- <Route path="/" component={Home} />
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
- ### Create Links to Your Routes
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
- Use an anchor tag that takes you to a route:
154
+ ## Route Definitions
192
155
 
193
- ```jsx
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
- render(
213
- () => (
214
- <Router root={App}>
215
- <Route path="/users" component={Users} />
216
- <Route path="/" component={Home} />
217
- </Router>
218
- ),
219
- document.getElementById("app")
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
- If you don't know the path ahead of time, you might want to treat part of the path as a flexible parameter that is passed on to the component.
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
- ```jsx
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
- const Users = lazy(() => import("./pages/Users"));
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
- render(
237
- () => (
238
- <Router>
239
- <Route path="/users" component={Users} />
240
- <Route path="/users/:id" component={User} />
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
- The colon indicates that `id` can be any string, and as long as the URL fits that pattern, the `User` component will show.
181
+ As long as the URL fits the pattern, the `User` component shows, and `id` is available via `useParams`.
249
182
 
250
- You can then access that `id` from within a route component with `useParams`.
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
- **Note on Animation/Transitions**:
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
- ```jsx
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
- const User = lazy(() => import("./pages/User"));
195
+ ```tsx
196
+ import { int, type MatchFilters } from "@solidjs/router";
273
197
 
274
198
  const filters: MatchFilters = {
275
- parent: ["mom", "dad"], // allow enum values
276
- id: /^\d+$/, // only allow numbers
277
- withHtmlExtension: (v: string) => v.length > 5 && v.endsWith(".html"), // we want an `*.html` extension
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
- render(
281
- () => (
282
- <Router>
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
- Here, we have added the `matchFilters` prop. This allows us to validate the `parent`, `id` and `withHtmlExtension` parameters against the filters defined in `filters`.
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
- So in this example:
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
- - `/users/mom/123/contact.html` would match,
300
- - `/users/dad/123/about.html` would match,
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
- Parameters can be specified as optional by adding a question mark to the end of the parameter name:
222
+ Add a question mark to make a parameter optional:
310
223
 
311
- ```jsx
224
+ ```tsx
312
225
  // Matches stories and stories/123 but not stories/123/comments
313
- <Route path="/stories/:id?" component={Stories} />
226
+ { path: "/stories/:id?", component: Stories }
314
227
  ```
315
228
 
316
229
  ### Wildcard Routes
317
230
 
318
- `:param` lets you match an arbitrary name at that point in the path. You can use `*` to match any end of the path:
231
+ Use `*` to match any remainder of the path, optionally naming it to expose it as a parameter:
319
232
 
320
- ```jsx
321
- // Matches any path that begins with foo, including foo/, foo/a/, foo/a/b/c
322
- <Route path="foo/*" component={Foo} />
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
- If you want to expose the wild part of the path to the component as a parameter, you can name it:
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
- Routes also support defining multiple paths using an array. This allows a route to remain mounted and not rerender when switching between two or more locations that it matches:
242
+ An array of paths lets a route stay mounted (no re-render) when switching between locations it matches:
336
243
 
337
- ```jsx
338
- // Navigating from login to register does not cause the Login component to re-render
339
- <Route path={["login", "register"]} component={Login} />
244
+ ```tsx
245
+ // Navigating from login to register does not re-render Login
246
+ { path: ["login", "register"], component: Login }
340
247
  ```
341
248
 
342
- ## Nested Routes
249
+ ### Nested Routes
343
250
 
344
- The following two route definitions have the same result:
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
- ```jsx
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> We love our users! </h1>
257
+ <h1>We love our users!</h1>
384
258
  {props.children}
385
- <A href="/">Back Home</A>
259
+ <a href={paths()}>Back Home</a>
386
260
  </div>
387
261
  );
388
262
  }
389
263
 
390
- <Route path="/users" component={PageWrapper}>
391
- <Route path="/" component={Users} />
392
- <Route path="/:id" component={User} />
393
- </Route>;
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
- The routes are still configured the same, but now the route elements will appear inside the parent element where the `props.children` was declared.
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
- ## Preload Functions
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
- 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.
290
+ ### Lazy Route Subtrees
417
291
 
418
- As its only argument, the preload function is passed an object that you can use to access route information:
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
- ```js
421
- import { lazy } from "solid-js";
422
- import { Route } from "@solidjs/router";
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
- const User = lazy(() => import("./pages/users/[id].js"));
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
- // preload function
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
- // Pass it in the route definition
432
- <Route path="/users/:id" component={User} preload={preloadUser} />;
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
- | key | type | description |
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
- A common pattern is to export the preload function and data wrappers that corresponds to a route in a dedicated `route.data.js` file. This way, the data function can be imported without loading anything else.
319
+ ## Typed Paths
442
320
 
443
- ```js
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
- // In the Route definition
450
- <Route path="/users/:id" component={User} preload={preloadUser} />;
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
- The `preload` function's return value is passed to the page component for any intent other than `"preload"`, allowing you to initialize data or alternatively use our new Data APIs:
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
- ## Data APIs
333
+ ## Links
456
334
 
457
- Keep in mind that these are entirely optional, but they demonstrate the power of our preload mechanism.
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
- ### `query`
337
+ Behavior modifiers are attributes, so they work identically in client, server-rendered, and third-party markup:
460
338
 
461
- To prevent duplicate fetching and to handle refetching triggers, we provide a query API that accepts a function and returns the same function.
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
- ```jsx
464
- const getUser = query(async (id) => {
465
- return (await fetch(`/api/users/${id}`)).json();
466
- }, "users"); // used as the query key + serialized arguments
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
- It is expected that the arguments to the query function are serializable.
354
+ Active and pending state is styled with CSS — one vocabulary for every kind of link:
470
355
 
471
- This query accomplishes the following:
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
- 1. It does deduping on the server for the lifetime of the request.
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
- Using it with preload function might look like:
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
- ```js
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
- // preload function
488
- function preloadUser({params, location}) {
489
- void getUser(params.id)
388
+ function preloadUser({ params, location }) {
389
+ void getUser(params.id);
490
390
  }
491
391
 
492
- // Pass it in the route definition
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
- Inside your page component you:
395
+ The preload function receives:
497
396
 
498
- ```jsx
499
- // pages/users/[id].js
500
- import { getUser } from ... // the query function
501
- import { createMemo } from "solid-js";
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
- export default function User(props) {
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
- Cached function has a few useful methods for getting the key that are useful for invalidation.
405
+ ## Data APIs
510
406
 
511
- ```ts
512
- let id = 5;
407
+ These are entirely optional, but they demonstrate the power of the preload mechanism.
513
408
 
514
- getUser.key; // returns "users"
515
- getUser.keyFor(id); // returns "users[5]"
516
- ```
409
+ ### `query`
517
410
 
518
- You can revalidate the query using the `revalidate` method or you can set `revalidate` keys on your response from your actions. If you pass the whole key it will invalidate all the entries for the query (ie "users" in the example above). You can also invalidate a single entry by using `keyFor`.
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
- `query` can be defined anywhere and then used inside your components with:
419
+ A query:
521
420
 
522
- ### Async reads in Solid 2
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
- On this Solid 2 branch, `query()` results are meant to be consumed directly with Solid primitives like `createMemo` and `createProjection`.
426
+ Consume results directly with Solid primitives — there is no router-specific async wrapper:
525
427
 
526
- ```jsx
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
- For object-shaped data where you want a deeply reactive result, use `createProjection`.
436
+ Keys support targeted invalidation:
532
437
 
533
- ```jsx
534
- const todos = createProjection(() => getTodos(), []);
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
- Router `action()` is the router-aware mutation wrapper for Solid 2. It keeps form submission, redirects, and invalidation wired into the router while letting you compose optimistic UI with Solid's built-in primitives.
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
- ```jsx
542
- import { action, revalidate, redirect } from "@solidjs/router"
449
+ ```tsx
450
+ import { action } from "@solidjs/router";
451
+ import { redirect } from "@solidjs/web";
452
+ import { paths } from "./router";
543
453
 
544
- // anywhere
545
- const myAction = action(async (data) => {
546
- await doMutation(data);
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
- // in component
551
- <form action={myAction} method="post" />
460
+ ```tsx
461
+ <form action={updateUser} method="post">
462
+ <button>Save</button>
463
+ </form>
552
464
 
553
- //or
554
- <button type="submit" formaction={myAction}></button>
465
+ // or
466
+ <button type="submit" formaction={updateUser}>Save</button>
555
467
  ```
556
468
 
557
- Actions only work with post requests, so make sure to put `method="post"` on your form.
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
- For optimistic updates, use Solid's optimistic primitives for the rendered state and attach owner-scoped submit hooks to the router action:
471
+ ```css
472
+ form[aria-busy] button { pointer-events: none; opacity: 0.6; }
473
+ ```
560
474
 
561
- ```jsx
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 (todo) => {
569
- await saveTodo(todo);
570
- return { ok: true, todo };
571
- }, "add-todo").onSubmit(todo => {
572
- setTodos(items => {
573
- items.push({ ...todo, pending: true });
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
- `myAction.onSubmit(...)` registers a listener for that action in the current reactive owner. Multiple components can register hooks against the same action, and those hooks are automatically removed when their owner is disposed. `myAction.onSettled(...)` works the same way for observing completed submissions.
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
- ```js
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
- Instead with `with` you can write this:
502
+ Actions have a `with` method (like `bind`) for typed arguments instead of hidden form fields:
599
503
 
600
- ```js
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
- Actions also take a second argument which can be the name or an option object with `name`. `name` is used to identify SSR actions that aren't server functions (see note below).
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
- Instead of forms you can use actions directly by wrapping them in a `useAction` primitive. This is how we get the router context.
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
- ```jsx
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
- This returns settled submission records for an action. It is useful for reading completed results, clearing old submissions, retrying a prior submission, or replaying settled errors. It is not the optimistic state layer.
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
- const submissions = useSubmissions(action, (input) => filter(input));
645
- const latestSubmission = submissions.at(-1);
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` or `createOptimisticStore` for in-flight UI, and use submissions as the durable settled record layer.
533
+ Use Solid's `createOptimistic` / `createOptimisticStore` for in-flight UI.
649
534
 
650
- ### Response Helpers
535
+ ## Typed Search Params
651
536
 
652
- These are used to communicate router navigations from query/actions, and can include invalidation hints. Generally these are thrown to not interfere the with the types and make it clear that function ends execution at that point.
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
- #### `redirect(path, options)`
539
+ ```tsx
540
+ import * as v from "valibot";
655
541
 
656
- Redirects to the next route
657
-
658
- ```js
659
- const getUser = query(() => {
660
- const user = await api.getCurrentUser()
661
- if (!user) throw redirect("/login");
662
- return user;
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
- #### `reload(options)`
667
-
668
- Reloads the data on the current page
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
- ```js
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
- ## Config Based Routing
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
- You don't have to use JSX to set up your routes; you can pass an array of route definitions:
564
+ ## Router Config Reference
685
565
 
686
- ```jsx
687
- import { lazy } from "solid-js";
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
- Also you can pass a single route definition object for a single route:
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
- ```jsx
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
- render(() => <Router>{route}</Router>, document.getElementById("app"));
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
- ## Alternative Routers
591
+ ## Router Primitives
743
592
 
744
- ### Hash Mode Router
593
+ Hooks read the live session off router context.
745
594
 
746
- By default, Solid Router uses `location.pathname` as route path. You can simply switch to hash mode through using `<HashRouter>`.
595
+ ### useParams
747
596
 
748
- ```jsx
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
- <HashRouter />;
599
+ ```tsx
600
+ const params = useParams(); // Params (strings)
601
+ const params = useParams(paths.users); // { id: number } — typed via matchFilters
752
602
  ```
753
603
 
754
- ### Memory Mode Router
604
+ ### useNavigate
755
605
 
756
- You can also use memory mode router for testing purpose.
606
+ Retrieves a method to navigate. Accepts a string or a typed path node, plus options:
757
607
 
758
- ```jsx
759
- import { MemoryRouter } from "@solidjs/router";
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
- <MemoryRouter />;
613
+ ```tsx
614
+ const navigate = useNavigate();
615
+ navigate(paths.login, { replace: true });
762
616
  ```
763
617
 
764
- ### SSR Routing
618
+ For declarative redirects on render (the old `<Navigate>`), call it during component setup or redirect from a preload.
765
619
 
766
- For SSR you can use the static router directly or the browser Router defaults to it on the server, just pass in the url.
620
+ ### useLocation
767
621
 
768
- ```jsx
769
- import { isServer } from "@solidjs/web";
770
- import { Router } from "@solidjs/router";
622
+ Retrieves the reactive `location` object:
771
623
 
772
- <Router url={isServer ? req.url : ""} />;
624
+ ```tsx
625
+ const location = useLocation();
626
+ const pathname = createMemo(() => parsePath(location.pathname));
773
627
  ```
774
628
 
775
- ## Components
776
-
777
- ### `<Router>`
629
+ ### useSearchParams
778
630
 
779
- This is the main Router component for the browser.
631
+ See [Typed Search Params](#typed-search-params). Reads are proxied — access properties to subscribe.
780
632
 
781
- | prop | type | description |
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
- ### `<A>`
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
- Like the `<a>` tag but supports automatic apply of base path + relative paths and active class styling (requires client side JavaScript).
637
+ ```tsx
638
+ const isRouting = useIsRouting();
639
+ return <div classList={{ "grey-out": isRouting() }}>...</div>;
640
+ ```
793
641
 
794
- The `<A>` tag has an `active` class if its href matches the current location, and `inactive` otherwise. **Note:** By default matching includes locations that are descendants (eg. href `/users` matches locations `/users` and `/users/123`), use the boolean `end` prop to prevent matching these. This is particularly useful for links to the root route `/` which would match everything.
642
+ ### useMatch
795
643
 
796
- | prop | type | description |
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
- ### `<Navigate />`
646
+ ```tsx
647
+ const match = useMatch(() => "/admin/*rest");
648
+ return <Show when={match()}>...</Show>;
649
+ ```
807
650
 
808
- Solid Router provides a `Navigate` component that works similarly to `A`, but it will _immediately_ navigate to the provided path as soon as the component is rendered. It also uses the `href` prop, but you have the additional option of passing a function to `href` that returns a path to navigate to:
651
+ ### useRouteMatches
809
652
 
810
- ```jsx
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
- // Navigating to /redirect will redirect you to the result of getPath
818
- <Route path="/redirect" component={() => <Navigate href={getPath} />} />;
655
+ ```tsx
656
+ const matches = useRouteMatches();
657
+ const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb));
819
658
  ```
820
659
 
821
- ### `<Route>`
660
+ ### usePreloadRoute
822
661
 
823
- The Component for defining Routes:
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
- | prop | type | description |
826
- | ------------ | ------------------ | ----------------------------------------------------------------- |
827
- | path | string | Path partial for defining the route segment |
828
- | component | `Component` | Component that will be rendered for the matched segment |
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
- ## Router Primitives
669
+ ### useLinkState
834
670
 
835
- Solid Router provides a number of primitives that read off the Router and Route context.
671
+ Reactive `active`/`current`/`pending` state for [custom link components](#links).
836
672
 
837
- ### useParams
673
+ ### useBeforeLeave
838
674
 
839
- Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
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
- ```js
842
- const params = useParams();
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
- // fetch user based on the id path parameter
845
- const [user] = createResource(() => params.id, fetchUser);
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
- ### useNavigate
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
- - resolve (_boolean_, default `true`): resolve the path against the current route
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
- **Note:** The state is serialized using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) which does not support all object types.
701
+ ```tsx
702
+ import { createRouter, hashHistory, memoryHistory } from "@solidjs/router";
858
703
 
859
- ```js
860
- const navigate = useNavigate();
704
+ // hash mode
705
+ const Router = createRouter({ routes, history: hashHistory() });
861
706
 
862
- if (unauthorized) {
863
- navigate("/login", { replace: true });
864
- }
707
+ // tests and non-browser environments
708
+ const Router = createRouter({ routes, history: memoryHistory("/users/1") });
865
709
  ```
866
710
 
867
- ### useLocation
868
-
869
- Retrieves reactive `location` object useful for getting things like `pathname`.
711
+ ### Environments without Proxy
870
712
 
871
- ```js
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
- const pathname = createMemo(() => parsePath(location.pathname));
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
- ### useSearchParams
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 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.
727
+ The instance also matches arbitrary URLs anywhere — server middleware, sitemap generation, tests — with no rendering involved:
882
728
 
883
- ```js
884
- const [searchParams, setSearchParams] = useSearchParams();
729
+ ```tsx
730
+ import { Router } from "./router";
885
731
 
886
- return (
887
- <div>
888
- <span>Page: {searchParams.page}</span>
889
- <button
890
- onClick={() =>
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
- ### useIsRouting
739
+ ## Server Integration
901
740
 
902
- Retrieves a signal that indicates whether the router is currently processing a navigation. Useful for showing pending navigation state while the next route and its data settle.
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
- ```js
905
- const isRouting = useIsRouting();
743
+ ```tsx
744
+ import { createFlightDataCollector, createNoJSHandler } from "@solidjs/router/server";
745
+ import { Router } from "./app/router";
906
746
 
907
- return (
908
- <div class={{ "grey-out": isRouting() }}>
909
- <MyAwesomeContent />
910
- </div>
911
- );
747
+ const collectFlightData = createFlightDataCollector(Router);
748
+ const handleNoJS = createNoJSHandler();
912
749
  ```
913
750
 
914
- ### useMatch
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
- ```js
919
- const match = useMatch(() => props.href);
920
-
921
- return <div class={{ active: Boolean(match()) }} />;
922
- ```
753
+ ## Migration from 0.x
923
754
 
924
- ### useCurrentMatches
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
- `useCurrentMatches` returns all the matches for the current matched route. Useful for getting all the route information.
757
+ ### Router components → `createRouter`
927
758
 
928
- For example if you stored breadcrumbs on your route definition you could retrieve them like so:
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
- ```js
931
- const matches = useCurrentMatches();
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
- const breadcrumbs = createMemo(() =>
934
- matches().map((m) => m.route.info.breadcrumb)
935
- );
774
+ <Router>{props => <App {...props} />}</Router>
936
775
  ```
937
776
 
938
- ### usePreloadRoute
939
-
940
- `usePreloadRoute` returns a function that can be used to preload a route manual. This is what happens automatically with link hovering and similar focus based behavior, but it is available here as an API.
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
- ### useBeforeLeave
782
+ ### JSX `<Route>` → config objects
949
783
 
950
- `useBeforeLeave` takes a function that will be called prior to leaving a route. The function will be called with:
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
- - from (_Location_): current location (before change).
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
- Example usage:
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
- ```js
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
- ## Migration from 0.16.x
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
- This branch is the Solid 2 migration. Most route configuration stays the same, but the data APIs and recommended async patterns have changed.
802
+ ### Data APIs (Solid 2)
979
803
 
980
- ### Async reads move to Solid 2 primitives
804
+ - `createAsync` / `createAsyncStore` are gone — read `query()` results with Solid 2 primitives: `createMemo`, `createProjection`, `createOptimistic`, `createOptimisticStore`.
981
805
 
982
- `createAsync` and `createAsyncStore` are gone. Read query results with Solid 2 primitives like `createMemo`, `createProjection`, `createOptimistic`, and `createOptimisticStore`.
806
+ ```tsx
807
+ // 0.x
808
+ const user = createAsync(() => getUser(params.id));
983
809
 
984
- ```jsx
810
+ // 1.0
985
811
  const user = createMemo(() => getUser(params.id));
986
- const [todos, setTodos] = createOptimisticStore(() => getTodos(), []);
987
812
  ```
988
813
 
989
- ### `query()` stays the source of truth
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
- The action API is now centered around instance methods:
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
- ```jsx
998
- const saveTodo = action(async (todo) => {
999
- await api.saveTodo(todo);
1000
- return { ok: true, todo };
1001
- }, "save-todo")
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
- .onSettled(submission => {
1006
- // observe settled result or retry state
1007
- });
828
+ );
1008
829
  ```
1009
830
 
1010
- - Use `onSubmit(...)` for owner-scoped optimistic/pre-submit work.
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 applications that use a client side router that does not rely on Server Side Rendering you need to handle redirects to your index page so that loading from other URLs does not cause your CDN or Hosting to return not found for pages that aren't actually there.
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
- Each provider has a different way of doing this. For example on Netlify you create a `_redirects` file that contains:
837
+ On Netlify, create a `_redirects` file:
1028
838
 
1029
839
  ```sh
1030
840
  /* /index.html 200
1031
841
  ```
1032
842
 
1033
- On Vercel you add a rewrites section to your `vercel.json`:
843
+ On Vercel, add a rewrites section to `vercel.json`:
1034
844
 
1035
845
  ```json
1036
846
  {