solid2-agent-kit 0.14.0 → 0.16.0
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 +9 -4
- package/bin/solid2-kit.mjs +218 -15
- package/files/commands/solid2-review.md +22 -3
- package/files/shared/agents-section.md +1 -1
- package/files/shared/rules-body.md +131 -46
- package/files/skills/solid-2/SKILL.md +353 -60
- package/files/skills/solid-2/references/official-docs.md +27 -0
- package/package.json +1 -1
|
@@ -41,7 +41,8 @@ Practical consequences:
|
|
|
41
41
|
| Expensive derivation, multiple consumers, or equality boundary needed | `createMemo(() => ...)` |
|
|
42
42
|
| Follows a prop but user can locally override; prop change resets | writable derivation: `createSignal(() => props.value)` / `createStore(() => props.value, fallback)` |
|
|
43
43
|
| Shared by a subtree, independent per provider instance | signal/store created inside a provider component, passed via context |
|
|
44
|
-
|
|
|
44
|
+
| App-wide state (theme, session, cart, locale) | the same provider, placed at the root of `App` — not a module-level signal/store (under SSR one module instance serves every request) |
|
|
45
|
+
| Constants (a category list, a formatter, the `createContext` call) | module scope |
|
|
45
46
|
| Tentative value during a mutation | `createOptimistic` / `createOptimisticStore` + `action` |
|
|
46
47
|
| Values pushed by an external subscription (websocket, reactive client) | async iterable returned from a memo (or function-form `createStore` for keyed reconciliation) — never `{ data, error }` signal pairs |
|
|
47
48
|
|
|
@@ -72,9 +73,9 @@ Most React `useEffect` code should NOT become `createEffect`:
|
|
|
72
73
|
| Overlay / modal | `<Portal>` — hoist async reads *above* the portal (reads inside start on the client) |
|
|
73
74
|
| Async value used several layers down | Create the memo high; pass `value={memo()}` through intermediates (they do not wait); put `<Loading>` around the leaf read |
|
|
74
75
|
| Nested child with its own fetch | Leave it nested — it runs in parallel. Sequential only when the second call needs the first response (`fetchAuthor(story().authorId)`) |
|
|
75
|
-
| Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render) |
|
|
76
|
+
| Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render; `{ export: "Chart" }` for a named export). Placeholder: `<Chart fallback={<Skeleton />} />` — SSR renders the fallback; a `<Loading>` around it leaves the server HTML empty |
|
|
76
77
|
| Browser-only *value* (`localStorage`, viewport size) | `createMemo(..., { ssrSource: "client" })` — per-value policy at the data source, no component split (`"hybrid"` when server data mixes with client signals) |
|
|
77
|
-
| Server vs browser
|
|
78
|
+
| Server vs browser code path | `isServer` / `isDev` from `@solidjs/web` (build-time constants), not `typeof window` — never to choose markup (`{isServer ? <A /> : <B />}` is a hydration mismatch; use `clientOnly` / `ssrSource`) |
|
|
78
79
|
| SSR-stable `id` / `for` / `aria-*` pairing | `createUniqueId()` |
|
|
79
80
|
|
|
80
81
|
## Canonical patterns
|
|
@@ -158,6 +159,32 @@ at component-body top level is a write in an owned scope — dev throws
|
|
|
158
159
|
`[REACTIVE_WRITE_IN_OWNED_SCOPE]` (rc.9 closed the exemption store writes had) — so
|
|
159
160
|
seed the shape through the `createStore` argument or a derivation.
|
|
160
161
|
|
|
162
|
+
**The store itself is read-only; only the draft writes.** An assignment or mutating
|
|
163
|
+
call on the store proxy outside a setter is *ignored* — no throw, no dev warning, the
|
|
164
|
+
value does not change, and TypeScript accepts it (`Store<T>` is `T`). This is the
|
|
165
|
+
MobX / Vue / Solid 1 `createMutable` reflex:
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
// WRONG — all silently ignored
|
|
169
|
+
todos.push(todo);
|
|
170
|
+
state.count++;
|
|
171
|
+
todo.done = !todo.done;
|
|
172
|
+
|
|
173
|
+
// CORRECT
|
|
174
|
+
setTodos((draft) => { draft.push(todo); });
|
|
175
|
+
setState((draft) => { draft.count++; });
|
|
176
|
+
setTodos((draft) => {
|
|
177
|
+
const row = draft.find((t) => t.id === id);
|
|
178
|
+
if (row) row.done = !row.done;
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Plain objects and arrays are proxied; `Map`, `Set`, and `Date` are stored as they are.
|
|
183
|
+
`draft.selected.add(id)` inside a setter mutates the `Set` but notifies nobody. Keep a
|
|
184
|
+
set of ids as `Record<string, true>` (`draft.selected[id] = true` /
|
|
185
|
+
`delete draft.selected[id]`, both tracked) or an array, or assign a new instance
|
|
186
|
+
(`draft.tags = new Set([...draft.tags, tag])`) so the property itself changes.
|
|
187
|
+
|
|
161
188
|
**External collections enter stores through reconciliation, never wholesale assignment.**
|
|
162
189
|
`draft.todos = serverTodos` renders correctly but replaces every object identity — all
|
|
163
190
|
subscribers under the path re-run and row identity is lost. In order of preference:
|
|
@@ -168,7 +195,10 @@ subscribers under the path re-run and row identity is lost. In order of preferen
|
|
|
168
195
|
`setState((draft) => { reconcile(fresh, 'id')(draft.todos); })`. Positional data
|
|
169
196
|
(fixed-shape dashboards): `reconcile(next, null)`.
|
|
170
197
|
|
|
171
|
-
Plain non-reactive
|
|
198
|
+
Plain non-reactive view for logging/serialization: `snapshot(store)`. It is not a copy —
|
|
199
|
+
subtrees with no pending changes are the store's own objects, so mutating the snapshot
|
|
200
|
+
(or letting a library `sort()` it) changes the store with no notification. Edit
|
|
201
|
+
`structuredClone(snapshot(store))` instead.
|
|
172
202
|
Subscribe an effect compute to every nested change: `deep(store)`.
|
|
173
203
|
|
|
174
204
|
### Children: pass through, or resolve with `children()`
|
|
@@ -248,6 +278,25 @@ createEffect(
|
|
|
248
278
|
);
|
|
249
279
|
```
|
|
250
280
|
|
|
281
|
+
`apply` returns a cleanup function or nothing — write it with a **block body**. An
|
|
282
|
+
expression-bodied arrow returns whatever the expression evaluates to: an assignment
|
|
283
|
+
(`(t) => (document.title = t)`) returns the string, and a setter (`(v) => setDraft(v)`)
|
|
284
|
+
returns the value it set. Dev throws `effect callback returned an invalid cleanup value`,
|
|
285
|
+
production throws a `TypeError` on the next run, and both halt the whole
|
|
286
|
+
reactive system (`[REACTIVITY_HALTED]`). TypeScript reports it as TS2345; a JS project
|
|
287
|
+
gets no warning. The cleanup of an effect is the `apply` return value — `onCleanup`
|
|
288
|
+
inside `apply` has no owner and never runs (`[NO_OWNER_CLEANUP]`).
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
// WRONG — returns the assigned string; halts reactivity (dev: first run, prod: next run)
|
|
292
|
+
createEffect(() => title(), (t) => (document.title = t));
|
|
293
|
+
|
|
294
|
+
// CORRECT
|
|
295
|
+
createEffect(() => title(), (t) => {
|
|
296
|
+
document.title = t;
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
251
300
|
Reads inside `apply` do not track. Extract every needed reactive value in `compute`
|
|
252
301
|
(e.g. `() => ({ name: user.name, role: user.role })`). Passing a store proxy into `apply`
|
|
253
302
|
and reading `user.name` there *runs once* and never retriggers. Compute-phase errors can be
|
|
@@ -454,7 +503,10 @@ registers this hook must load before the server graph — put it in the plugin's
|
|
|
454
503
|
`start: { instrument: "./src/instrument.ts" }`, not at the top of an entry (see
|
|
455
504
|
[Production observability](#production-observability-the-observe-build)).
|
|
456
505
|
|
|
457
|
-
`render` / `hydrate` accept a per-root `onError` that wins over the ambient hook
|
|
506
|
+
`render` / `hydrate` accept a per-root `onError` that wins over the ambient hook —
|
|
507
|
+
`render(() => <App />, root, undefined, { onError })` (options are the 4th argument; a
|
|
508
|
+
3rd-argument object is taken as `init` and the hook is silently not installed) and
|
|
509
|
+
`hydrate(() => <App />, root, { onError })`.
|
|
458
510
|
Uncaught errors (`REACTIVITY_HALTED`) go to the platform's `reportError`, which every
|
|
459
511
|
monitor already listens on — do not report them twice. `renderToStream` /
|
|
460
512
|
`renderToString`'s `onError` option *is* the per-render server hook: it now hears every
|
|
@@ -572,12 +624,28 @@ function createTodos() { // call from a component — not module scope (SSR shar
|
|
|
572
624
|
Three layers, in this order: durable source → ephemeral UI that must survive
|
|
573
625
|
overlay discard (the `errors` map, folded in the projection) → optimistic overlay
|
|
574
626
|
(`pending`, the predicted `completed`). Consumers read one store. Catch
|
|
575
|
-
*expected* mutation failures in the action
|
|
576
|
-
|
|
577
|
-
|
|
627
|
+
*expected* mutation failures in the action, because nothing else will show them:
|
|
628
|
+
an error that escapes an action discards its optimistic writes and rejects the
|
|
629
|
+
promise the call returned — it never reaches `<Errored>`, which catches failing
|
|
630
|
+
reads and renders only (projection and render errors still reach the boundary).
|
|
631
|
+
Uncaught, the UI silently reverts and the rejection is unhandled. Put expected
|
|
632
|
+
failures in the errors map or a toast inside the action, and `.catch` the call in
|
|
633
|
+
the handler for the rest:
|
|
634
|
+
|
|
635
|
+
```tsx
|
|
636
|
+
<button
|
|
637
|
+
type="button"
|
|
638
|
+
onClick={() => {
|
|
639
|
+
removeTodo(todo.id).catch(() => showToast('Could not delete'));
|
|
640
|
+
}}
|
|
641
|
+
>
|
|
642
|
+
Delete
|
|
643
|
+
</button>
|
|
644
|
+
```
|
|
578
645
|
|
|
579
646
|
Going from a client store to the server is **additive**: same `setTodos`
|
|
580
|
-
calls, wrap each mutation in `action`, swap in
|
|
647
|
+
calls, wrap each mutation in `action`, swap in the **function form**
|
|
648
|
+
`createOptimisticStore(() => api.list(), [])` (with `refresh` after the `yield`) and a
|
|
581
649
|
file of server functions. Do not rewrite `App.tsx` with loading/error branches,
|
|
582
650
|
and do not snapshot-the-cache / write-a-prediction / restore-on-error. A cache
|
|
583
651
|
library is not the productionizing step — the sync mutation already was the
|
|
@@ -594,6 +662,16 @@ Ordinary signal/store writes inside an action are held until it settles. Never c
|
|
|
594
662
|
Do not set a `submitted` signal and watch it from an effect — that work belongs in the
|
|
595
663
|
handler or the `action`.
|
|
596
664
|
|
|
665
|
+
**The value form has no durable layer.** `createOptimisticStore([])` (or
|
|
666
|
+
`createOptimistic(value)`) is a pure overlay: writes inside an action vanish when it
|
|
667
|
+
settles, and writes outside an action do not stick. Copying the reference example
|
|
668
|
+
`createOptimisticStore<Todo[]>([])` and writing the saved row after the `yield`
|
|
669
|
+
leaves an empty list once the action settles, with no warning. Durable data comes
|
|
670
|
+
from the function form — `createOptimisticStore(() => api.list(), [])` refreshed
|
|
671
|
+
after the `yield`, or `createOptimisticStore(() => base.todos, [])` over a separate
|
|
672
|
+
`createStore` that the action writes the confirmed value into. Keep the value form for
|
|
673
|
+
in-flight flags.
|
|
674
|
+
|
|
597
675
|
`createOptimistic` / `createOptimisticStore` are for a tentative value during an
|
|
598
676
|
active mutation, not a local editing session (use a writable derivation or a plain
|
|
599
677
|
store for that). A whole-form `createOptimistic(false)` saving flag is fine; prefer
|
|
@@ -772,9 +850,47 @@ function ThemeButton() {
|
|
|
772
850
|
|
|
773
851
|
Pass accessors/setters/stores through context — never `value={theme()}` (a dead snapshot).
|
|
774
852
|
No `useMemo` for the value object, no context splitting: the object is created once and
|
|
775
|
-
fine-grained updates flow through the signals inside it.
|
|
776
|
-
|
|
777
|
-
|
|
853
|
+
fine-grained updates flow through the signals inside it.
|
|
854
|
+
|
|
855
|
+
**App-wide state is the same provider, placed at the root of `App`** — not a
|
|
856
|
+
module-level signal or store. Module scope is for constants and the `createContext`
|
|
857
|
+
call itself:
|
|
858
|
+
|
|
859
|
+
```tsx
|
|
860
|
+
import { createContext, createStore, useContext, type ParentProps } from 'solid-js';
|
|
861
|
+
|
|
862
|
+
type CartItem = { id: string; quantity: number };
|
|
863
|
+
|
|
864
|
+
// WRONG under SSR — one store for every request the server ever handles
|
|
865
|
+
// export const [cart, setCart] = createStore({ items: [] as CartItem[] });
|
|
866
|
+
|
|
867
|
+
// CORRECT — one store per provider instance: per tab in the browser, per request on the server
|
|
868
|
+
function createCart() {
|
|
869
|
+
const [cart, setCart] = createStore({ items: [] as CartItem[] });
|
|
870
|
+
return { cart, setCart };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const CartContext = createContext<ReturnType<typeof createCart>>();
|
|
874
|
+
|
|
875
|
+
export function CartProvider(props: ParentProps) {
|
|
876
|
+
return <CartContext value={createCart()}>{props.children}</CartContext>;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export function useCart() {
|
|
880
|
+
return useContext(CartContext); // throws ContextNotFoundError outside CartProvider
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// App: <CartProvider><ThemeProvider>{/* router / pages */}</ThemeProvider></CartProvider>
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
Module state has no owner (nothing disposes it), and under `ssr: true` the server
|
|
887
|
+
loads the module once and serves every request from it: `createStore(value)` on the
|
|
888
|
+
server is the plain object, so one visitor's writes render into the next visitor's
|
|
889
|
+
HTML (`[SERVER_WRITE]`). A module-level store derived from a server function fails
|
|
890
|
+
sooner — `Cannot call server function outside of a request` while the module loads.
|
|
891
|
+
In a client-only build a module-level store is one per tab and works, which is why it
|
|
892
|
+
looks fine until `ssr: true` is turned on; the provider costs nothing extra and
|
|
893
|
+
survives that change.
|
|
778
894
|
|
|
779
895
|
### Refs
|
|
780
896
|
|
|
@@ -815,6 +931,32 @@ do not `return () => cleanup`. Put owned setup in a **directive factory** (like
|
|
|
815
931
|
`ResizeObserver`, third-party widgets, and native listener options (`capture` /
|
|
816
932
|
`passive`) — Solid event props do not take those options.
|
|
817
933
|
|
|
934
|
+
### `onSettled` — one-time setup, restricted scope
|
|
935
|
+
|
|
936
|
+
`onSettled` is the `onMount` replacement, and its callback is a **restricted scope**:
|
|
937
|
+
`onCleanup` throws there (`[CLEANUP_IN_FORBIDDEN_SCOPE]`), creating a memo or effect
|
|
938
|
+
throws (`[PRIMITIVE_IN_FORBIDDEN_SCOPE]`), and so does `flush()` — each halts the
|
|
939
|
+
reactive system in dev. Return the cleanup; create effects and memos in the component
|
|
940
|
+
body, next to the `onSettled` call, not inside it.
|
|
941
|
+
|
|
942
|
+
```tsx
|
|
943
|
+
// WRONG — the mechanical 1.x port of onMount + onCleanup
|
|
944
|
+
onSettled(() => {
|
|
945
|
+
const id = setInterval(tick, 1000);
|
|
946
|
+
onCleanup(() => clearInterval(id));
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
// CORRECT
|
|
950
|
+
onSettled(() => {
|
|
951
|
+
const id = setInterval(tick, 1000);
|
|
952
|
+
return () => clearInterval(id);
|
|
953
|
+
});
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
Effect callbacks follow the same shape: return the cleanup from `apply` (an
|
|
957
|
+
`onCleanup` there never runs), and `flush()` inside one is a no-op
|
|
958
|
+
(`[FLUSH_IN_EFFECT_CALLBACK]`).
|
|
959
|
+
|
|
818
960
|
### Per-value SSR policy: `ssrSource`
|
|
819
961
|
|
|
820
962
|
`clientOnly` splits a *component*; `ssrSource` keeps one *value* off the server,
|
|
@@ -893,6 +1035,14 @@ test('increments on click', () => {
|
|
|
893
1035
|
});
|
|
894
1036
|
```
|
|
895
1037
|
|
|
1038
|
+
Primitive tests write **outside** the root callback — a write inside `createRoot((dispose)
|
|
1039
|
+
=> { …; setA(2); })` is `[REACTIVE_WRITE_IN_OWNED_SCOPE]` in dev. Return the handles, then
|
|
1040
|
+
write and `flush()`: `const { setA, b, dispose } = createRoot((dispose) => { …; return
|
|
1041
|
+
{ setA, b, dispose }; }); setA(2); flush();`. A hook that reads context runs under
|
|
1042
|
+
`renderHook(useCart, { wrapper: CartProvider })`. A routed component gets a router built in
|
|
1043
|
+
the test — `createRouter({ routes, history: memoryHistory("/orders/42") })` — because
|
|
1044
|
+
Testing Library's `render(…, { location })` is typed but not implemented (1.0.0-beta.3).
|
|
1045
|
+
|
|
896
1046
|
`jsxImportSource` for tests and app code is `"@solidjs/web"`, not `"solid-js"`.
|
|
897
1047
|
Vite plugin is `@solidjs/vite-plugin`, not `vite-plugin-solid`.
|
|
898
1048
|
|
|
@@ -1151,22 +1301,28 @@ Solid Router 0.x/1.x stand-ins.** Patterns below; fetch the matching pages from
|
|
|
1151
1301
|
```
|
|
1152
1302
|
|
|
1153
1303
|
`solid-js` does **not** export a `JSX` namespace — `import type { JSX } from "solid-js"`
|
|
1154
|
-
is a type error (TS2305)
|
|
1155
|
-
|
|
1304
|
+
is a type error (TS2305). Markup types are `JSX.Element` from `@solidjs/web` (the
|
|
1305
|
+
docs' choice; it also admits DOM nodes) or the renderer-neutral `Element` from `solid-js`
|
|
1156
1306
|
(`children?: Element`), alongside `Component` / `ParentProps` / `Accessor` / `Setter`.
|
|
1157
|
-
|
|
1158
|
-
|
|
1307
|
+
Import the latter as `import type { Element as SolidElement } from "solid-js"` in any file
|
|
1308
|
+
that also uses DOM elements — unaliased, it shadows the DOM `Element`
|
|
1309
|
+
(`el.getBoundingClientRect()` becomes TS2339). DOM-specific `JSX` types
|
|
1310
|
+
(`JSX.IntrinsicElements`, `JSX.CSSProperties`) and `ComponentProps` come from
|
|
1311
|
+
`@solidjs/web`. Vite: `import solid from "@solidjs/vite-plugin"`
|
|
1159
1312
|
with `start: true` / `ssr: true` / `serverFunctions: true` only when the
|
|
1160
1313
|
project already uses start mode. Tiers are `bare` → `basic` (router) →
|
|
1161
1314
|
`fullstack` (SSR + server functions); do not jump a tier without cause.
|
|
1162
|
-
Under `start: true` the plugin owns entries
|
|
1163
|
-
|
|
1164
|
-
or a hand-written `index.html`.
|
|
1315
|
+
Under `start: true` the plugin owns entries and the build — write `src/App.tsx`, not
|
|
1316
|
+
SolidStart-style `entry-client.tsx` / `entry-server.tsx` (authored ones replace the
|
|
1317
|
+
generated entries) or a hand-written `index.html`. The document shell (`<html lang>`,
|
|
1318
|
+
favicon, fallback `<title>`) is an optional `src/Document.tsx` default export that renders
|
|
1319
|
+
`<HydrationScript />` in `<head>` and `<body>{props.children}</body>`; `App` never renders
|
|
1320
|
+
`<html>`.
|
|
1165
1321
|
|
|
1166
1322
|
### Solid Router 2 — `createRouter`, not JSX `<Route>`
|
|
1167
1323
|
|
|
1168
1324
|
```tsx
|
|
1169
|
-
import { lazy } from 'solid-js';
|
|
1325
|
+
import { Loading, lazy } from 'solid-js';
|
|
1170
1326
|
import { createRouter } from '@solidjs/router';
|
|
1171
1327
|
|
|
1172
1328
|
export const Router = createRouter({
|
|
@@ -1184,7 +1340,9 @@ export default function App() {
|
|
|
1184
1340
|
{(props) => (
|
|
1185
1341
|
<>
|
|
1186
1342
|
<a href={Router.paths}>Home</a>
|
|
1187
|
-
<
|
|
1343
|
+
<Loading fallback={<p>Loading…</p>}>
|
|
1344
|
+
<main>{props.children}</main>
|
|
1345
|
+
</Loading>
|
|
1188
1346
|
</>
|
|
1189
1347
|
)}
|
|
1190
1348
|
</Router>
|
|
@@ -1192,7 +1350,9 @@ export default function App() {
|
|
|
1192
1350
|
}
|
|
1193
1351
|
```
|
|
1194
1352
|
|
|
1195
|
-
|
|
1353
|
+
The `<Loading>` covers the first load of the lazy pages (and lets SSR stream the shell);
|
|
1354
|
+
later navigations hold the current page while the next one loads (links get
|
|
1355
|
+
`data-pending`). Create the instance at **module scope**. Nested layouts are `children` arrays
|
|
1196
1356
|
on the route objects, not nested `<Route>` / nested routers. Solid Router does
|
|
1197
1357
|
**not** support nested `<Router>` instances — compose one route tree (or a lazy
|
|
1198
1358
|
`children` thunk). Navigate with
|
|
@@ -1200,8 +1360,57 @@ plain `<a href={Router.paths.about}>` (or `useNavigate`); there is no `<A>` /
|
|
|
1200
1360
|
`<Navigate>` / `<HashRouter>` / `<FileRoutes />`. Do not assign
|
|
1201
1361
|
`window.location.href` or call `history.pushState` for in-app navigation.
|
|
1202
1362
|
Session location is `useLocation` / `useParams`,
|
|
1203
|
-
not `Router.paths`.
|
|
1204
|
-
|
|
1363
|
+
not `Router.paths`.
|
|
1364
|
+
|
|
1365
|
+
**Route `preload` starts reads; the page reads them in a memo.** `preload` runs when
|
|
1366
|
+
the route matches (and on link hover), so it should kick off the query and return
|
|
1367
|
+
nothing; the component reads the same query through a memo that tracks the params:
|
|
1368
|
+
|
|
1369
|
+
```tsx
|
|
1370
|
+
// src/data/products.ts
|
|
1371
|
+
import { query } from '@solidjs/router';
|
|
1372
|
+
|
|
1373
|
+
export const getProduct = query(async (id: string) => {
|
|
1374
|
+
const response = await fetch(`/api/products/${id}`);
|
|
1375
|
+
return (await response.json()) as Product;
|
|
1376
|
+
}, 'product');
|
|
1377
|
+
|
|
1378
|
+
// src/router.ts — defineRoute types params from the path (params.id: string)
|
|
1379
|
+
import { lazy } from 'solid-js';
|
|
1380
|
+
import { createRouter, defineRoute } from '@solidjs/router';
|
|
1381
|
+
import { getProduct } from './data/products';
|
|
1382
|
+
|
|
1383
|
+
export const Router = createRouter({
|
|
1384
|
+
routes: [
|
|
1385
|
+
defineRoute({
|
|
1386
|
+
path: '/products/:id',
|
|
1387
|
+
preload: ({ params }) => void getProduct(params.id),
|
|
1388
|
+
component: lazy(() => import('./pages/Product')),
|
|
1389
|
+
}),
|
|
1390
|
+
],
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
// src/pages/Product.tsx
|
|
1394
|
+
import { createMemo } from 'solid-js';
|
|
1395
|
+
import type { RouteProps } from '@solidjs/router';
|
|
1396
|
+
import { getProduct } from '../data/products';
|
|
1397
|
+
|
|
1398
|
+
export default function ProductPage(props: RouteProps<'/products/:id'>) {
|
|
1399
|
+
const product = createMemo(() => getProduct(props.params.id));
|
|
1400
|
+
return <h1>{product().name}</h1>;
|
|
1401
|
+
}
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
Do not return the data from `preload` and render `props.data` (the Remix-loader
|
|
1405
|
+
shape): `props.data` is whatever `preload` returned when the route first matched, so
|
|
1406
|
+
navigating `/products/mug` → `/products/bowl` keeps showing the mug, and an `async`
|
|
1407
|
+
preload makes it a Promise (`props.data.name` is TS2339). Return a value only for
|
|
1408
|
+
data that stays fixed while the route is matched. The factory `preload` on
|
|
1409
|
+
`createRouter` runs once per mount/request; its result is the root render prop's
|
|
1410
|
+
`props.data`. Plain route objects type `params` as an open record (`string |
|
|
1411
|
+
undefined` per key) — wrap a route in `defineRoute` when `preload` or the component
|
|
1412
|
+
needs `params.id: string`.
|
|
1413
|
+
|
|
1205
1414
|
Link warming: `preload="false"` skips that link's *data* preload; `preloadLinks: false`
|
|
1206
1415
|
disables automatic link preloading. Intent values are `"initial"` / `"navigate"` /
|
|
1207
1416
|
`"native"` / `"preload"`. One router per app.
|
|
@@ -1225,7 +1434,9 @@ setSearch] = useSearchParams()` — the setter merges and navigates without scro
|
|
|
1225
1434
|
`""` / `undefined` / `null` removes a key. For typed values, put a synchronous
|
|
1226
1435
|
Standard Schema on the route's `search` field and pass the path node:
|
|
1227
1436
|
`useSearchParams(Router.paths.search)` (async schemas throw; a failing schema leaves
|
|
1228
|
-
the raw strings rather than throwing).
|
|
1437
|
+
the raw strings rather than throwing). Query values are strings, so coerce in the schema —
|
|
1438
|
+
`page: v.optional(v.pipe(v.unknown(), v.transform(Number)), 1)`; a plain `v.number()`
|
|
1439
|
+
rejects every value and hands back the string, still typed `number`.
|
|
1229
1440
|
|
|
1230
1441
|
**Two different `action`s.** Core `action` from `solid-js` is a generator transaction
|
|
1231
1442
|
(optimistic writes that span an async gap). Router `action` from `@solidjs/router` is
|
|
@@ -1250,6 +1461,14 @@ const save = action(async (form: FormData) => {
|
|
|
1250
1461
|
Only POST forms are accepted. Bind extra args with `.with(id)` (they go in the
|
|
1251
1462
|
action URL). `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` *runs* and
|
|
1252
1463
|
drops the no-JS form fallback. `useAction` is JS-only for the same reason.
|
|
1464
|
+
Optimistic rows for a router form keep the `<form>` and add no core `action`: read the
|
|
1465
|
+
list `query` with `createOptimisticStore(() => getList(), [])` and register
|
|
1466
|
+
`save.onSubmit((form) => setList((d) => { d.push({ …, pending: true }); }))` in the
|
|
1467
|
+
component — the hook runs inside the action's transaction, so the row is an overlay,
|
|
1468
|
+
replaced on settle by the revalidated `query` (a plain server-function read is not
|
|
1469
|
+
revalidated, so the row just disappears).
|
|
1470
|
+
`useSubmissions` keeps only completions with a result or error; observe void and
|
|
1471
|
+
redirect completions with `save.onSettled(hook)`.
|
|
1253
1472
|
|
|
1254
1473
|
Validated forms come in three passes sharing one HTML shape. Pass 1 (above) posts
|
|
1255
1474
|
uncontrolled inputs with browser `required` / `pattern` — it works with no JavaScript.
|
|
@@ -1349,7 +1568,15 @@ A function-level server function cannot close over component locals — pass
|
|
|
1349
1568
|
values as arguments. Treat every argument as untrusted — TypeScript does not
|
|
1350
1569
|
cross the HTTP boundary, so do not invent tRPC / RPC type-gen;
|
|
1351
1570
|
validate inside the function (a parse in the function body is that
|
|
1352
|
-
validation, not an RPC schema across the wire).
|
|
1571
|
+
validation, not an RPC schema across the wire). Checks belong in the body: HTTP dispatch
|
|
1572
|
+
calls a function-level `"use server"` body directly, so a HOF around it (`withAuth(fn)`,
|
|
1573
|
+
`validated(schema, fn)`) runs for in-process calls and is skipped for `curl` — an auth
|
|
1574
|
+
bypass. A wrapper inside a file-level `"use server"` module (`export const x =
|
|
1575
|
+
withAccount(async …)`) is what gets registered and does run. Arguments are capped at 1 MiB
|
|
1576
|
+
by default (413 beyond; the host's `bodySizeLimit`) — upload large files to a signed URL.
|
|
1577
|
+
Never hand a server function point-free to a library callback (`mutationFn: renameUser`,
|
|
1578
|
+
`onClick={deleteTodo}`): every argument the callback receives (a context object, the
|
|
1579
|
+
event) is sent over the wire — wrap it in an arrow that passes only the data. These functions *are* the RPC — do not add an
|
|
1353
1580
|
API-route file (or a Next `route.ts`) just to wrap `db.todos.insert`. API
|
|
1354
1581
|
routes are for real HTTP endpoints (webhooks, third-party POST), not for the
|
|
1355
1582
|
app's own RPC. Read trusted identity
|
|
@@ -1358,8 +1585,42 @@ reference runs in-process (no HTTP); in the browser it is HTTP. Anything
|
|
|
1358
1585
|
referenced only inside the `"use server"` body (db client, secrets) never
|
|
1359
1586
|
reaches the client — the directive is the privacy boundary. Mutations that should
|
|
1360
1587
|
revalidate: `redirect` / `reload` from `@solidjs/web` (see the server-function
|
|
1361
|
-
guides).
|
|
1362
|
-
|
|
1588
|
+
guides).
|
|
1589
|
+
|
|
1590
|
+
**`redirect()` / `reload()` are signals for an integration, not navigation.** Solid
|
|
1591
|
+
Router's `action` / `query` apply them (navigate, revalidate), and a no-JS form post
|
|
1592
|
+
follows the 302. Any other caller — a core `action`, an event handler, a plain
|
|
1593
|
+
`createMemo(() => listOrders())` — receives the raw `Response` as the resolved value:
|
|
1594
|
+
no navigation happens, and the type still claims your data. (When a mounted router's
|
|
1595
|
+
single-flight hook intercepts a direct POST call, it does navigate, but the call
|
|
1596
|
+
resolves to `null`; a direct `GET()` read always gets the `Response`.) Either way the
|
|
1597
|
+
caller never gets its data, so the redirect guard belongs to functions called through
|
|
1598
|
+
the router:
|
|
1599
|
+
|
|
1600
|
+
```ts
|
|
1601
|
+
import { getRequestEvent, redirect, respond } from '@solidjs/web';
|
|
1602
|
+
|
|
1603
|
+
// called through a router action/query: the router navigates to /sign-in
|
|
1604
|
+
export async function listOrders() {
|
|
1605
|
+
'use server';
|
|
1606
|
+
const userId = getRequestEvent()?.locals.userId;
|
|
1607
|
+
if (!userId) throw redirect('/sign-in');
|
|
1608
|
+
return database.orders.forUser(userId);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// no router in the project (core action + server functions): reject instead
|
|
1612
|
+
export async function listMyOrders() {
|
|
1613
|
+
'use server';
|
|
1614
|
+
const userId = getRequestEvent()?.locals.userId;
|
|
1615
|
+
if (!userId) throw respond(null, { status: 401 });
|
|
1616
|
+
return database.orders.forUser(userId);
|
|
1617
|
+
}
|
|
1618
|
+
```
|
|
1619
|
+
|
|
1620
|
+
Without the router, fail with `throw respond(..., { status })` (the call rejects and
|
|
1621
|
+
`<Errored>` / the action's `catch` sees it), or return a value and navigate in the
|
|
1622
|
+
caller. Return a redirect when it is the successful outcome, throw it for an early
|
|
1623
|
+
exit. `redirect()` and `respond()` accept the same `revalidate`
|
|
1363
1624
|
option as `reload()` when the mutation also changes location or returns a value;
|
|
1364
1625
|
`revalidate: REVALIDATE_ALL` (`"*"`, from `@solidjs/web`) declares every cache entry
|
|
1365
1626
|
stale, distinct from omitting it (the host's default) and from `[]` (nothing). After a router/server mutation do **not** follow with a client `fetch`
|
|
@@ -1370,11 +1631,17 @@ on the server-function wire **and**, since 2.0.0-rc.9, on every SSR road a failu
|
|
|
1370
1631
|
takes (an `<Errored>` fallback rendered on the server, a rejected async source
|
|
1371
1632
|
serialized into the stream, a `<Loading>` fragment's rejection), so `err().message`
|
|
1372
1633
|
in a fallback reads the generic message in a production server render. Use
|
|
1373
|
-
`markSafeError` or `respond(..., { status })` for intentional
|
|
1374
|
-
failures
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1634
|
+
`throw markSafeError(...)` or `throw respond(..., { status })` for intentional
|
|
1635
|
+
client-facing failures — a *returned* `respond()` resolves the call whatever its
|
|
1636
|
+
status, so `return respond({ error }, { status: 400 })` reads as success to the UI,
|
|
1637
|
+
`useSubmissions`, and the optimistic overlay. Do not `return Response.json(...)` from
|
|
1638
|
+
`"use server"` — that is HTTP-handler control flow; `return respond(value, { status: 201 })`
|
|
1639
|
+
is success metadata a scripted caller unwraps. Arguments are JSON: send `Date` / `Map` /
|
|
1640
|
+
`Set` as ISO strings / arrays (they throw otherwise; one `File` / `Blob` / `FormData`
|
|
1641
|
+
argument travels natively). `enableRichArguments()` from
|
|
1642
|
+
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that, but
|
|
1643
|
+
in rc.9 importing it fails `vite build` (`"./client" is not exported` — the dev server
|
|
1644
|
+
works, so the break shows only at build).
|
|
1378
1645
|
`GET()` is only for idempotent reads (URLs leak into logs/history);
|
|
1379
1646
|
import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
|
|
1380
1647
|
Reads are function calls too: `GET(async (id) => { "use server"; ... })` is invoked
|
|
@@ -1467,19 +1734,33 @@ When the app has a server bundle (`ssr` or server functions), production is
|
|
|
1467
1734
|
`handleRequest(request)` from the built `dist/server/server.js` (or that
|
|
1468
1735
|
module's Fetchable default `fetch`) — not an import from `solid-js`. Cloudflare /
|
|
1469
1736
|
Netlify / Nitro Vite plugins adopt that handler — do not write a Solid adapter,
|
|
1470
|
-
a custom Worker, or a Netlify Function. For Node,
|
|
1471
|
-
`server.js` (
|
|
1472
|
-
Express/
|
|
1737
|
+
a custom Worker, or a Netlify Function. For Node, set `start: { node: true }` and run
|
|
1738
|
+
`node dist/server/node.js` (serves `dist/client`, reads `PORT` / `HOST`); inside an
|
|
1739
|
+
existing Express / Fastify app, mount the `listener` that file exports — do not
|
|
1740
|
+
hand-write a Node↔web bridge (`node dist/server/server.js` starts nothing: it is the
|
|
1741
|
+
handler module). Client-only `start: true` is still static `dist/client`
|
|
1473
1742
|
— there is no handler to wrap. Request middleware is the Vite option
|
|
1474
1743
|
`start: { middleware: "./src/middleware.ts" }` (web `Request` + `next`), not
|
|
1475
1744
|
Express `app.use`. Server functions that return components
|
|
1476
1745
|
(`serverFunctions: { components: true }`) are experimental preview — do not use
|
|
1477
1746
|
them unless the project already has that flag on.
|
|
1478
1747
|
|
|
1479
|
-
Unscripted forms
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1748
|
+
Unscripted (no-JS) POST forms go through a router action —
|
|
1749
|
+
`const save = action(createTodo, "create-todo")` from `@solidjs/router`, then
|
|
1750
|
+
`<form action={save} method="post">` — not a client `preventDefault` + `fetch`, not a
|
|
1751
|
+
hand-built `/_server/` URL, and not `action={createTodo.url}`: a bare `"use server"`
|
|
1752
|
+
function keeps its declared function type, so `.url` is a type error (TS2339) even
|
|
1753
|
+
though it exists at runtime (and `serverFunctionActionUrl(createTodo)` is TS2345).
|
|
1754
|
+
The post arrives as one `FormData` argument, so the function behind `action={save}`
|
|
1755
|
+
takes `(form: FormData)` and parses and validates the fields inside; any other
|
|
1756
|
+
signature makes `action={save}` a type error (TS2322). Bind leading JSON-safe
|
|
1757
|
+
arguments with `action={save.with(id)}` for `(id: string, form: FormData)`; a function
|
|
1758
|
+
that already has a typed signature gets a separate `FormData` server function that
|
|
1759
|
+
parses and calls it.
|
|
1760
|
+
`.url` is typed only on `GET()` / `live()` references, which makes it the address of a
|
|
1761
|
+
`method="get"` search form (`action={searchProducts.url}`; the function receives the
|
|
1762
|
+
fields as `URLSearchParams`). GET forms are only for idempotent search — do not use a
|
|
1763
|
+
GET form for a mutation.
|
|
1483
1764
|
|
|
1484
1765
|
### Document head — no MetaProvider
|
|
1485
1766
|
|
|
@@ -1515,8 +1796,11 @@ how to reuse. Prefer the form on the right.
|
|
|
1515
1796
|
| `action(async function* () { await save(); setX(v); })` | `yield save()` or `await save(); yield; setX(v)` |
|
|
1516
1797
|
| `action(async (item) => { ... })` with core `action` from `solid-js` | `action(function* (item) { ...; yield ...; ... })` — only a generator re-enters the transaction (`solid2-kit check` flags this). Router `action` from `@solidjs/router` is the one that takes `async (form) => ...` |
|
|
1517
1798
|
| `todos()` on a store | `todos.length` / `todo.title` (property reads) |
|
|
1799
|
+
| `todos.push(x)` / `state.count++` / `todo.done = !todo.done` on the store itself | `setTodos((draft) => { draft.push(x); })` — writes to the store proxy outside a setter are silently ignored (no error, no warning, tsc passes) |
|
|
1800
|
+
| `createStore({ selected: new Set<string>() })` + `draft.selected.add(id)` | `selected: {} as Record<string, true>` + `draft.selected[id] = true` / `delete draft.selected[id]`, or `draft.selected = new Set([...draft.selected, id])` — `Map` / `Set` / `Date` are stored as-is, not tracked |
|
|
1518
1801
|
| `draft.todos = fresh` (wholesale fresh-tree assignment) | `reconcile(fresh, "id")`, or function-form `createStore` / `createProjection` (auto-keyed by `"id"`). Removal via `setTodos((t) => t.filter(...))` is fine — survivors keep identity |
|
|
1519
|
-
| `createEffect(() => user, (u) => log(u.name))` | `createEffect(() => user.name, (name) => log(name))` |
|
|
1802
|
+
| `createEffect(() => user, (u) => { log(u.name); })` | `createEffect(() => user.name, (name) => { log(name); })` |
|
|
1803
|
+
| `createEffect(() => title(), (t) => (document.title = t))` / `(v) => setX(v)` | `(t) => { document.title = t; }` — apply returns a cleanup or nothing; any other return value halts reactivity. An apply that only calls a local setter is rule 4 even with braces (`solid2-kit check` flags it): use a (writable) derivation |
|
|
1520
1804
|
| `<Loading fallback={<PageSkeleton />}>{/* header + data */}</Loading>` | wrap only the data slot; chrome stays outside |
|
|
1521
1805
|
| `<Loading on={id} fallback={...}>` (the accessor) | `on={id()}` — a value, so identity changes can show fallback |
|
|
1522
1806
|
| `setCount(count() + 1)` when writes can batch | `setCount((c) => c + 1)` |
|
|
@@ -1553,15 +1837,17 @@ how to reuse. Prefer the form on the right.
|
|
|
1553
1837
|
| `clientOnly` split (or `isServer` branch) for one browser-only *value* | `createMemo(() => localStorage.getItem(k), { ssrSource: "client" })` — `"hybrid"` when server data mixes with client signals |
|
|
1554
1838
|
| `await flush()` / `flush()` to wait on a pending memo | `await resolve(() => value())` (or Testing Library async queries); `flush()` only drains staged *sync* writes |
|
|
1555
1839
|
| `action(function* () { setX(v); })` around a navigation-shaped setter | plain setter; async computeds hold previous values. Core `action` only when writes happen *after* async work |
|
|
1556
|
-
| `
|
|
1840
|
+
| `export const [cart, setCart] = createStore(...)` for app-wide state | `createCart()` called by a `CartProvider` at the root of `App`, read with `useCart()` over `createContext<T>()` — module state has no owner and, under SSR, one instance serves every request (`[SERVER_WRITE]`). Module scope is for constants and `createContext` |
|
|
1557
1841
|
| `import { GET } from "@solidjs/start"` / `createAsync` / `useSubmission` / `<FileRoutes />` / `"use client"` | `GET` from `@solidjs/web/server-functions`; `createMemo(() => getUser(id()))`; `useSubmissions`; `fileRoutes(pageRoutes)`; Solid has no `"use client"` |
|
|
1558
1842
|
| `<Show keyed when={user()}>` by default | default `Show` (keeps children mounted); `keyed` only when internal state must reset |
|
|
1559
1843
|
| overlapping truthy `<Match>`es all expected to render | first truthy `<Match>` wins; later matches are skipped |
|
|
1560
1844
|
| missing `<HydrationScript />` / one script for many roots | once, before app markup, when the app owns the document; distinct `renderId` per extra root |
|
|
1561
|
-
| `Router.paths` as the current URL
|
|
1845
|
+
| `Router.paths` as the current URL | `useLocation` / `useParams` |
|
|
1846
|
+
| `preload: async ({ params }) => getProduct(params.id)` + `props.data.name` in the page | `preload: ({ params }) => void getProduct(params.id)` + `createMemo(() => getProduct(props.params.id))` — `props.data` is captured once when the route matches (stale on param change; a Promise for async preloads) |
|
|
1562
1847
|
| `query.get(key)` with no guaranteed entry; `query.set(key, promise)` | `query.get` throws if missing; `query.set` does not accept a promise |
|
|
1563
1848
|
| `onCleanup(() => ...)` in a component body | `onSettled(() => { ...; return cleanup })` — `onCleanup` is for custom primitives |
|
|
1564
|
-
| `onSettled` with reactive reads expecting re-runs | each call is a **single** fire; reads are untracked. Ongoing work is `createEffect
|
|
1849
|
+
| `onSettled` with reactive reads expecting re-runs | each call is a **single** fire; reads are untracked. Ongoing work is `createEffect`, created in the component body |
|
|
1850
|
+
| `onSettled(() => { const id = setInterval(tick, 1000); onCleanup(() => clearInterval(id)); })` | `onSettled(() => { const id = setInterval(tick, 1000); return () => clearInterval(id); })` — `onCleanup`, `createMemo` / `createEffect`, and `flush()` throw inside the callback |
|
|
1565
1851
|
| `<Router>` nested inside `<Router>` | one instance; nest `children` arrays (or a lazy children thunk) |
|
|
1566
1852
|
| `const View = tab() ? A : B; return <View />` | `const View = dynamic(() => (tab() ? A : B))` — the ternary at setup freezes the choice |
|
|
1567
1853
|
| `<For each={resolved()}>` after `children()` | `{resolved.toArray()}` — resolved children are not a reactive data list |
|
|
@@ -1571,8 +1857,8 @@ how to reuse. Prefer the form on the right.
|
|
|
1571
1857
|
| `createOptimistic` for a local edit draft | writable derivation / plain store. Optimistic is for an in-flight mutation |
|
|
1572
1858
|
| `createMemo(async fn, { loadingValue })` / `{ seedLoadingValue: true }` as the default first-flight UI | `<Loading>` for first flight; those options are escape hatches (store projections use `seedLoadingValue`) |
|
|
1573
1859
|
| `setSubmitted(true)` + an effect that watches it | do the work in the handler or an `action` |
|
|
1574
|
-
| `findUser(userId)` with the id from the client as identity | `getRequestEvent()
|
|
1575
|
-
| `action={"/_server/" + id}`
|
|
1860
|
+
| `findUser(userId)` with the id from the client as identity | `const userId = getRequestEvent()?.locals.userId; if (!userId) throw respond(null, { status: 401 });` (`throw redirect("/sign-in")` under a router `action` / `query`) |
|
|
1861
|
+
| `action={"/_server/" + id}` / `action={createTodo.url}` on a POST form | a router action: `const save = action(createTodo, "create-todo")` + `<form action={save} method="post">` over a `(form: FormData)` server function (other signatures are TS2322) — `.url` on a bare `"use server"` function is TS2339; it is typed only on `GET()` / `live()` references (GET search forms) |
|
|
1576
1862
|
| `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
|
|
1577
1863
|
| `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
|
|
1578
1864
|
| `class={{ active: location.pathname === "/about" }}` hand-rolled link state | CSS on automatic `aria-current` / `data-active` / `data-pending`; `useLinkState` in JSX, `useIsRouting()` for progress bars |
|
|
@@ -1587,7 +1873,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1587
1873
|
| `class={{ pending: isPending(id) }}` flashing on every navigation | same class + a short CSS `transition-delay` so only slow swaps show |
|
|
1588
1874
|
| `<Comments />` nested under `{story().title}` assumed to waterfall | both mount immediately and fetch in parallel if the child reads an already-known id; a real waterfall is `createMemo(() => fetchAuthor(story().authorId))` or `storyId={props.story.id}` |
|
|
1589
1875
|
| `<Errored>` as a terminal ErrorBoundary; `reset={() => location.reload()}` | the boundary heals when the source succeeds again; `reset` retries collected *sources*, not a UI remount |
|
|
1590
|
-
| `<Errored>` around a list
|
|
1876
|
+
| `<Errored>` around a list expected to catch a toggle/save failure | an error thrown out of an `action` never reaches `<Errored>` — it reverts the overlay and rejects the call's promise. Catch expected failures in the `action` (row errors in a map the projection folds in, which survives overlay discard; otherwise a toast) and `.catch` the call in the handler |
|
|
1591
1877
|
| `yield { ...tick, connected: true }` from `live()` | yield the value; wire status is `source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`) |
|
|
1592
1878
|
| sibling `<Loading>` fallbacks stacking as popcorn | `<Reveal collapsed>` (sequential default) suppresses tail skeletons past the frontier |
|
|
1593
1879
|
| in-memory cache around a GET server function | `return respond(value, { headers: { "cache-control": "..." } })` — metadata rides HTTP, the value rides the graph |
|
|
@@ -1599,6 +1885,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1599
1885
|
| a held write with no feedback anywhere | pair it with `isPending()` / `latest()` / an optimistic value / `affects()` — otherwise dev + attribution reports `[SILENT_HOLD]` |
|
|
1600
1886
|
| `refresh(getUser.key)` / `revalidate(user)` / `return refresh()` from `"use server"` | three APIs: core `refresh(source)` reruns a reactive source; router `revalidate(getUser.key)` invalidates the query cache; `return reload({ revalidate: "todos" })` asks the integration to refresh cached data |
|
|
1601
1887
|
| `return redirect(...)` as the only redirect shape | guards `throw redirect("/sign-in")`; `redirect()` / `respond()` take `revalidate` too when the mutation also moves or returns a value |
|
|
1888
|
+
| `throw redirect("/sign-in")` / `return reload(...)` in a function called directly (core `action`, handler, plain memo) | only Solid Router `action` / `query` (and no-JS form posts) apply them — a direct caller resolves to the raw `Response` (or `null` when the router's single-flight hook intercepts a POST call), typed as your data. Without the router: `throw respond(null, { status: 401 })` or return a value and navigate in the caller |
|
|
1602
1889
|
| `submission.clear()` from an effect | on dismiss/resubmit only — from an effect, errors flash and vanish |
|
|
1603
1890
|
| `revalidate(liveSource)` | `live()` updates through the open stream; do not revalidate it |
|
|
1604
1891
|
| `refresh(user)` without `affects` when the reload should look pending | pair `affects(user)` with `refresh(user)` — a bare `refresh` re-asks quietly |
|
|
@@ -1606,7 +1893,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1606
1893
|
| `setStore(async (draft) => { draft.x = await load(); })` | `const x = await load(); setStore((draft) => { draft.x = x; })` — the setter is a synchronous transaction; dev throws `[ASYNC_STORE_SETTER]` |
|
|
1607
1894
|
| `setStore((d) => { d.items = seed; })` in a component body | seed through `createStore(seed)` / `createStore(() => props.items, fallback)` — a body-level store write is `[REACTIVE_WRITE_IN_OWNED_SCOPE]` |
|
|
1608
1895
|
| `const saved = await api.save(); yield until(() => ...)` | `await api.save(); yield; yield until(() => ...)` — a bare `yield` re-enters the transaction before any reader is created, not only before writes |
|
|
1609
|
-
| `fallback={(err) => { captureException(err()); return <Fallback />; }}` | `configureClientErrors({ onError })` from `solid-js` (or `render(
|
|
1896
|
+
| `fallback={(err) => { captureException(err()); return <Fallback />; }}` | `configureClientErrors({ onError })` from `solid-js` (or per root `render(fn, root, undefined, { onError })`), `configureServerErrors({ onError })` from `@solidjs/web` — once per error, with `ownerPath` / `boundaryPath` |
|
|
1610
1897
|
| `latest(user) ?? placeholder` in a handler as a null-safe read | `latest()` throws `NotReadyError` before the first value in every scope; read settled values under `<Loading>`, or `await until(() => user())` |
|
|
1611
1898
|
| `attribution.why(total)` / `attribution.format(e)` / `attribution.costs()` | named imports from `solid-js/attribution`: `why(total)`, `formatRerun(e)`, `costs()`, `feedback()`, `subscriptions(scope)` (rc.9) |
|
|
1612
1899
|
| `attribution.enable()` unguarded in app code, or `enable()` with the default `log: true` left on | `if (isDev) attribution.enable({ log: false })` — on the observe build an unguarded `enable()` records in production; prefer the `/__solid/diagnostics` session (`begin` enables, `end` disables) so app code carries nothing |
|
|
@@ -1629,18 +1916,19 @@ how to reuse. Prefer the form on the right.
|
|
|
1629
1916
|
| `fetch('/api')` at component-body top level | `createMemo(async () => { ... fetch ... })` under `<Loading>` — a top-level fetch runs once at mount and is not a reactive source |
|
|
1630
1917
|
| `<input type="checkbox" value={on()} />` | `checked={on()}` for the toggle; `value="string"` only when grouping radios or listing submitted values |
|
|
1631
1918
|
| `createMemo(() => { void save(); return x(); })` / an effect that calls an action | invoke actions from handlers, not from memos or effects |
|
|
1632
|
-
| `createSignal(props.value)` / `createStore(props.items)` | `createSignal(() => props.value)` / `createStore(() => props.items, fallback)` — a bare `props.x` at setup is a snapshot |
|
|
1633
|
-
| `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)` |
|
|
1919
|
+
| `createSignal(props.value)` / `createStore(props.items)` | `createSignal(() => props.value)` / `createStore(() => props.items, fallback)` — a bare `props.x` at setup is a snapshot. That form resets on every prop change; an intentional one-time read (an `initialCount` prop) is `createSignal(untrack(() => props.initialCount))` |
|
|
1920
|
+
| `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)`; `createTrackedEffect` is `@deprecated` (rc.9) |
|
|
1634
1921
|
| `createEffect(() => source(), () => setError(null))` to reset a signal | `createSignal(() => { source(); return null; })` — the writable derivation resets on change; a sole-setter apply is rule 4 |
|
|
1635
1922
|
| a "server-safe" initial value adopted after mount (React hydration-mismatch fix) | client mode mounts with `render()` into an empty body — no hydration, no mismatch; read `localStorage` / `matchMedia` directly at signal creation |
|
|
1636
|
-
| rewrite `App.tsx` with loading/error branches, or snapshot/restore, when the list moves to the server | same `setTodos`; wrap mutations in `action`; swap `createOptimisticStore` + a server-functions file. The overlay is discarded; the confirmed store is the truth |
|
|
1923
|
+
| rewrite `App.tsx` with loading/error branches, or snapshot/restore, when the list moves to the server | same `setTodos`; wrap mutations in `action`; swap in function-form `createOptimisticStore(() => api.list(), [])` + a server-functions file. The overlay is discarded; the confirmed store is the truth |
|
|
1924
|
+
| `createOptimisticStore<Todo[]>([])` holding the list, saved rows written after `yield` | `createOptimisticStore(() => api.list(), [])` + `refresh(todos)` (or a function form over a separate `createStore`) — the value form is a pure overlay: action writes vanish on settle |
|
|
1637
1925
|
| disable an optimistic row until ack, or mutex rapid clicks / freeze unrelated writes | keep the control enabled; `action` is the transaction. An in-flight action does not freeze other writes. Failed-action replay is optional |
|
|
1638
1926
|
| tRPC / RPC type-gen around `"use server"` | TypeScript does not cross HTTP — validate inside the function. Locals only the body reads (db, secrets) stay off the client |
|
|
1639
1927
|
| `src/routes/api/todos.ts` (or a Next `route.ts`) wrapping `db.todos.insert` | `"use server"` *is* the RPC. API routes are for real HTTP endpoints (webhooks), not the app's own mutations |
|
|
1640
1928
|
| router/server mutation, then `fetch` / `onSettled` / `hydrate` to refresh the page | `reload` / `revalidate` / single-flight (the mutation response can seed destination preloads). In-flight Promises serialize as Promises; `live()` continues from HTML |
|
|
1641
1929
|
| delay `renderToStream` so HTML arrives in visual order | stream as soon as ready; `<Reveal collapsed>` controls when content *appears* |
|
|
1642
1930
|
| subscribe/unsubscribe in the component around `live()` | `createMemo(() => stockPrice(props.symbol))` — hoist that memo to share one connection. `onstatus` is on the iterable the `live()` call returns, not on the memo. Until the first yield, `<Loading>`; later yields are updates |
|
|
1643
|
-
| a Solid adapter / custom Worker / Express bridge, or `entry-client.tsx` under `start: true` | With a server bundle: `handleRequest` from `dist/server/server.js` / Fetchable `fetch`; platform plugins adopt it. Node:
|
|
1931
|
+
| a Solid adapter / custom Worker / Express bridge, or `entry-client.tsx` under `start: true` | With a server bundle: `handleRequest` from `dist/server/server.js` / Fetchable `fetch`; platform plugins adopt it. Node: `start: { node: true }` → `node dist/server/node.js` (its `listener` for an existing Express / Fastify app). Client-only start: static `dist/client`. Middleware is `start: { middleware: "..." }`, not Express `app.use` |
|
|
1644
1932
|
| `createMemo(async () => (await props.story).author)` | `createMemo(() => props.story.author)` — a derivation over an async value becomes async; no await, no Promise type |
|
|
1645
1933
|
| return a component from `"use server"` / flip `serverFunctions.components` | experimental preview — do not enable unless the project already has that flag |
|
|
1646
1934
|
|
|
@@ -1779,17 +2067,19 @@ always-applied rules installed alongside this skill.
|
|
|
1779
2067
|
bind `latest(query)`; failures throw to `<Errored>`, never `{ success: false }`
|
|
1780
2068
|
result objects checked at every read.
|
|
1781
2069
|
- [ ] Lists via `<For>` (server/refetched rows keyed by stable id), conditionals via
|
|
1782
|
-
ternary/`<Show>`; no `{list().map(...)}` in JSX; no `key` props
|
|
2070
|
+
ternary/`<Show>`; no `{list().map(...)}` in JSX; no `key` props (except
|
|
2071
|
+
`@solidjs/meta` tags); no `value()!`.
|
|
1783
2072
|
- [ ] Effects are two-phase and only at imperative boundaries; apply does not read stores.
|
|
1784
2073
|
- [ ] Async reads sit under `<Loading>` (the data slot, not chrome); errors under `<Errored>`.
|
|
1785
2074
|
Reactive inputs of an async memo are read before the first `await`. No hand-rolled
|
|
1786
2075
|
`loading`/`error` signals or `=== undefined` readiness branches. First load is
|
|
1787
2076
|
`<Loading>`; refetch indicator is `isPending(user)` (the accessor, not `user()`)
|
|
1788
2077
|
or `isPending(selectedId)` when the write is the question. `<Errored>` heals
|
|
1789
|
-
when the source succeeds; `reset` retries sources, not a UI remount.
|
|
1790
|
-
|
|
1791
|
-
the
|
|
1792
|
-
|
|
2078
|
+
when the source succeeds; `reset` retries sources, not a UI remount. Mutation
|
|
2079
|
+
failures never reach `<Errored>` (an escaping error reverts the overlay and
|
|
2080
|
+
rejects the call): catch them in the action/projection, and `.catch` the call
|
|
2081
|
+
in the handler. Error monitoring goes through `configureClientErrors` /
|
|
2082
|
+
`configureServerErrors` (or `render(fn, root, undefined, { onError })`), never a side effect in
|
|
1793
2083
|
the fallback. Core `refresh(source)`, router `revalidate(key)`, and `return reload(...)` are
|
|
1794
2084
|
different APIs — do not mix them. Client → server is additive (same
|
|
1795
2085
|
setters + `action` + server functions): do not rewrite App with
|
|
@@ -1807,13 +2097,16 @@ always-applied rules installed alongside this skill.
|
|
|
1807
2097
|
- [ ] Rest props via `omit`, not `{ ...props }`. Conditional `class` via objects, not string concat.
|
|
1808
2098
|
- [ ] Input filters write back to the DOM on reject (inputs do not rewind).
|
|
1809
2099
|
- [ ] No Solid 1.x imports or APIs (`solid-js/store`, `createResource`, `onMount`, `Suspense`, ...).
|
|
1810
|
-
No `JSX` imported from `solid-js` —
|
|
1811
|
-
`solid-js
|
|
2100
|
+
No `JSX` imported from `solid-js` — markup types are `JSX.Element` from
|
|
2101
|
+
`@solidjs/web` or `Element` from `solid-js` (aliased next to DOM code).
|
|
1812
2102
|
- [ ] Inspect children with `children()`; code-split with `lazy` + `<Loading>`; reactive
|
|
1813
2103
|
component choice with `dynamic()`. No `React.lazy`, no effects inside ref callbacks.
|
|
1814
2104
|
- [ ] Browser-only code uses `isServer` / `clientOnly` (components) / `ssrSource`
|
|
1815
2105
|
(values), not `typeof window`.
|
|
1816
|
-
Component teardown is `onSettled` (return cleanup), not `onCleanup
|
|
2106
|
+
Component teardown is `onSettled` (return cleanup), not `onCleanup`; nothing
|
|
2107
|
+
inside the `onSettled` callback calls `onCleanup`, `flush()`, or creates a
|
|
2108
|
+
memo/effect. Effect `apply` functions have block bodies and return only a
|
|
2109
|
+
cleanup or nothing.
|
|
1817
2110
|
- [ ] Dev console shows no diagnostics: reads in tracking scopes (no
|
|
1818
2111
|
`[STRICT_READ_UNTRACKED]`), store setters synchronous and never called at
|
|
1819
2112
|
body level (no `[ASYNC_STORE_SETTER]` / `[REACTIVE_WRITE_IN_OWNED_SCOPE]`), held
|