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
|
@@ -42,7 +42,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
42
42
|
before a flush, use the setter updater (`setCount((c) => c + 1)`); `setCount(count() + 1)`
|
|
43
43
|
reads the last *committed* value and drops the other staged writes. To store a *function*
|
|
44
44
|
in a signal, wrap it: `setHandler(() => myHandler)` — `setHandler(myHandler)` treats it
|
|
45
|
-
as an updater.
|
|
45
|
+
as an updater. The initial value too: `createSignal(() => myHandler)` —
|
|
46
|
+
`createSignal(myHandler)` is the derived form (it calls `myHandler` and stores the result).
|
|
46
47
|
5. `class`, not `className`; `for`, not `htmlFor`. `class` accepts strings, conditional
|
|
47
48
|
objects, and nested arrays: `class={["btn", { active: selected() }]}`. Put *conditional*
|
|
48
49
|
names in an object — `class={`btn ${on() ? "on" : ""}`}`, `clsx(...)`, and
|
|
@@ -60,13 +61,15 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
60
61
|
that writes the event object; wrap it: `onClick={() => setCount((c) => c + 1)}`.
|
|
61
62
|
Debounce at the handler (`onInput={debounce(...)}`), never as an effect copying
|
|
62
63
|
one signal into another.
|
|
63
|
-
7. **Lists use `<For>`**, never `{list().map(...)}` in reactive JSX and never `key` props.
|
|
64
|
+
7. **Lists use `<For>`**, never `{list().map(...)}` in reactive JSX and never `key` props (only `@solidjs/meta` tags take `key`, as the tag's identity).
|
|
64
65
|
`{todos().map((t) => <Row todo={t} />)}` *renders*, then recreates every row on each
|
|
65
66
|
update. Row identity: default = item reference; `keyed={(item) => item.id}` = key function
|
|
66
67
|
(child receives item as an accessor); `keyed={false}` = positional. **Rows from
|
|
67
68
|
server/refetched data (fresh object references on every update — fetch results,
|
|
68
69
|
subscription payloads) must use a key function on a stable id**, or every update
|
|
69
|
-
recreates every row (dev + attribution flags the rebuilds as `[UNSTABLE_LIST_IDENTITY]`).
|
|
70
|
+
recreates every row (dev + attribution flags the rebuilds as `[UNSTABLE_LIST_IDENTITY]`).
|
|
71
|
+
Usually better still: load them into a function-form store (rule 20), whose proxies keep
|
|
72
|
+
identity per id, and keep default keying. Reference keying is for local arrays whose item identities are
|
|
70
73
|
stable (e.g. store rows). Match the child signature to the mode: default item is the
|
|
71
74
|
**raw** value (`todo.title`); `keyed={(t) => t.id}` / `keyed={false}` pass an
|
|
72
75
|
**accessor** (`todo().title`). Mixing those still “runs” (you render a function, or
|
|
@@ -75,9 +78,16 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
75
78
|
fed to `<For>` (that rebuilds the window array every time).
|
|
76
79
|
8. **Effects have two phases**: `createEffect(compute, apply)`. All reactive reads go in
|
|
77
80
|
`compute`; its return value feeds `apply`, which does imperative work and may return a
|
|
78
|
-
cleanup.
|
|
79
|
-
`
|
|
80
|
-
|
|
81
|
+
cleanup. `apply` returns a cleanup function or nothing — **write it with a block body**:
|
|
82
|
+
`(t) => { document.title = t; }`, never `(t) => (document.title = t)` or
|
|
83
|
+
`(v) => setX(v)` (an assignment or setter returns its value; dev throws
|
|
84
|
+
`invalid cleanup value`, production fails on the next run, and either halts the whole
|
|
85
|
+
reactive system). An `apply` that only calls a local setter is still rule 4 with a
|
|
86
|
+
block body — make it a derivation. Register an effect's cleanup by returning it
|
|
87
|
+
from `apply` — `onCleanup` there never runs (`[NO_OWNER_CLEANUP]`).
|
|
88
|
+
Single-argument `createEffect(fn)` is an error in Solid 2. Do not substitute
|
|
89
|
+
`createTrackedEffect` for that — it is `@deprecated` since 2.0.0-rc.9 (kept only for
|
|
90
|
+
1.x migration). Skip the initial run with `{ defer: true }`. Most React `useEffect` code should not become an effect at all — see the skill.
|
|
81
91
|
9. **Stores update by mutating a draft**: `setStore(draft => { draft.user.name = "Ada" })`.
|
|
82
92
|
Never rebuild with spreads — that destroys property-level subscriptions. The return
|
|
83
93
|
form is for shapes where mutation is awkward — most commonly removal
|
|
@@ -86,6 +96,15 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
86
96
|
`reconcile` or the derived form (dev + attribution flags the store-level waste as
|
|
87
97
|
`[IMMUTABLE_UPDATE_IN_STORE]`). Stores are
|
|
88
98
|
**property reads** (`todos.length`, `todo.title`), not accessors — never `todos()`.
|
|
99
|
+
The setter takes only a function: `setState("a", v)` and the 1.x shallow merge
|
|
100
|
+
`setState({ a: 1 })` throw `fn is not a function`, and an object *returned* from the
|
|
101
|
+
callback replaces the whole store (keys it omits are deleted).
|
|
102
|
+
The store itself is read-only: `todos.push(x)`, `state.count++`, or
|
|
103
|
+
`todo.done = !todo.done` outside a setter is **silently ignored** (no throw, no
|
|
104
|
+
warning, and TypeScript accepts it) — every write goes through the setter's draft.
|
|
105
|
+
`Map`, `Set`, and `Date` are stored as-is, not proxied: `draft.selected.add(id)`
|
|
106
|
+
changes the object but notifies nobody. Model sets as `Record<id, true>` (or an
|
|
107
|
+
array), or assign a new instance in the setter (`draft.tags = new Set([...draft.tags, t])`).
|
|
89
108
|
The setter callback is a **synchronous** transaction: `setStore(async (draft) => …)`
|
|
90
109
|
throws `[ASYNC_STORE_SETTER]` in dev (writes after the first `await` were silently
|
|
91
110
|
lost before). `await` first, then call the setter (inside an `action`, `yield` first).
|
|
@@ -103,13 +122,17 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
103
122
|
`<Child user={user()} />` then `{props.user.name}` under the child's
|
|
104
123
|
`<Loading>` is the colorless form. `const u = user()` at the parent *is* a
|
|
105
124
|
read and throws/snapshots there. Pass the memo itself only when the child
|
|
106
|
-
must `refresh()` that source —
|
|
125
|
+
must `refresh()` that source — type that prop `SourceAccessor<User>` (`Accessor<User>`
|
|
126
|
+
is TS2345 at `refresh`) — `isPending(() => props.user)` works on the
|
|
107
127
|
value. Read every reactive input **before the first
|
|
108
128
|
`await`** — post-`await` reads do not subscribe, and in production the computation can
|
|
109
129
|
sit pending with no retry. No `useEffect` + `setState` fetching, no `createResource`.
|
|
110
130
|
`<Loading>` wraps the data slot, not page chrome. After first paint it keeps content
|
|
111
131
|
during refetch (`isPending` for the indicator). Use `on={id()}` (the *value*, not the
|
|
112
|
-
accessor) only when that identity change should show the fallback again.
|
|
132
|
+
accessor) only when that identity change should show the fallback again. `on` compares
|
|
133
|
+
with `!==` (several keys: one string such as `` `${a()}/${b()}` ``, never a fresh array)
|
|
134
|
+
and takes effect only when no reader outside the boundary waits on the same write
|
|
135
|
+
(that is how one slow panel stops holding the page). Do not start `fetch` (or any request) at component-body top
|
|
113
136
|
level — that runs once at mount and is not a reactive source. Do not `try/catch` `NotReadyError` around a read, and do not
|
|
114
137
|
use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
|
|
115
138
|
`<Loading>`. `{latest(() => x())}` is a preview, not the visible answer, and not a
|
|
@@ -143,9 +166,16 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
143
166
|
`createContext<T>()` without a default throws when read outside a provider (good) — use
|
|
144
167
|
that for any reactive payload. `createContext("light")` is only for a primitive static
|
|
145
168
|
fallback; a dummy default on a store/signal context *runs* and then silently no-ops
|
|
146
|
-
without a provider.
|
|
147
|
-
|
|
148
|
-
|
|
169
|
+
without a provider. **Shared state lives in a provider, including app-wide state**
|
|
170
|
+
(theme, session, cart, locale): a factory such as `createCart()` called by a provider
|
|
171
|
+
component placed at the root of `App`, read through `createContext<T>()` (no default)
|
|
172
|
+
and a `useCart()` hook. Module scope is for constants and the `createContext` call itself —
|
|
173
|
+
never `export const [session, setSession] = createStore(...)`. Module state has no
|
|
174
|
+
owner, and under `ssr: true` one module instance serves every request, so one
|
|
175
|
+
visitor's state renders into another's HTML (`[SERVER_WRITE]`); a module-level
|
|
176
|
+
store derived from a server function throws `Cannot call server function outside
|
|
177
|
+
of a request` at import. A client-only build happens to work, until the day SSR is
|
|
178
|
+
turned on.
|
|
149
179
|
12. Refs: `let el!: HTMLDivElement` + `ref={el}` or `ref={(node) => (el = node)}`, or forward
|
|
150
180
|
`props.ref`. Compose with arrays: `ref={[props.ref, (node) => (el = node)]}`. No
|
|
151
181
|
`useRef`/`.current`. Ref callbacks run untracked and **without an owner**; their
|
|
@@ -155,12 +185,18 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
155
185
|
`onSettled(() => { ...; return cleanup })` — `onCleanup` is for custom
|
|
156
186
|
primitives and captured owners, not component bodies. Each `onSettled`
|
|
157
187
|
registers a **single** fire (reads inside are untracked); ongoing
|
|
158
|
-
imperative work is `createEffect
|
|
188
|
+
imperative work is `createEffect`, created in the component body — not inside
|
|
189
|
+
the callback. The `onSettled` callback is a restricted scope: `onCleanup`
|
|
190
|
+
(`[CLEANUP_IN_FORBIDDEN_SCOPE]`), creating a memo or effect
|
|
191
|
+
(`[PRIMITIVE_IN_FORBIDDEN_SCOPE]`), and `flush()` all throw there in dev and halt
|
|
192
|
+
the reactive system. A 1.x `onMount(() => { …; onCleanup(…) })` becomes
|
|
193
|
+
`onSettled(() => { const id = setInterval(tick, 1000); return () => clearInterval(id); })`.
|
|
159
194
|
13. **Writes are staged** and commit on the next microtask. Event handlers need nothing
|
|
160
195
|
special; tests and imperative integration code must call `flush()` before observing
|
|
161
196
|
updated *synchronous* state or DOM. Waiting on an async memo is
|
|
162
197
|
`await resolve(() => value())` (or Testing Library async queries), not `flush()`.
|
|
163
|
-
Never call `flush()` inside an `action
|
|
198
|
+
Never call `flush()` inside an `action`, an `onSettled` callback (throws in dev), or an
|
|
199
|
+
effect callback (a no-op, `[FLUSH_IN_EFFECT_CALLBACK]`).
|
|
164
200
|
14. Do not port these React tools — they have no Solid equivalent because the problems they
|
|
165
201
|
solve don't exist: `useCallback`, `React.memo`, `forwardRef`, `useSyncExternalStore`,
|
|
166
202
|
dependency arrays, `startTransition`/`useTransition` (updates are held and coordinated
|
|
@@ -170,8 +206,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
170
206
|
JSX (`<Show>`, ternary, `<Switch>`/`<Match>`). Early returns on genuinely non-reactive
|
|
171
207
|
values (build-time config, missing env) are fine. `<Switch>` is first-wins: later
|
|
172
208
|
truthy `<Match>`es are skipped, not “all matching branches”.
|
|
173
|
-
16.
|
|
174
|
-
|
|
209
|
+
16. Plain text is ordinary JSX children (`<p>{message()}</p>`); `textContent` is for an
|
|
210
|
+
element whose entire content is one text value, never together with children. Use
|
|
211
|
+
`innerHTML` only for trusted or sanitized markup — never interpolate user input into it, never React's
|
|
175
212
|
`dangerouslySetInnerHTML`, and never combine `innerHTML` with JSX children (they fight
|
|
176
213
|
over the same contents).
|
|
177
214
|
17. **Never narrow a reactive read with a non-null assertion** (`error()!.message`,
|
|
@@ -190,17 +227,23 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
190
227
|
by `<Errored>`; speculative value = `latest()`. `<Errored>` is graph status, not a
|
|
191
228
|
terminal React ErrorBoundary: it heals when the source succeeds again (refresh,
|
|
192
229
|
live reconnect, input change). `reset` retries the collected *sources*, not a UI
|
|
193
|
-
remount.
|
|
194
|
-
|
|
230
|
+
remount. `<Errored>` catches failing *reads* and renders, never a mutation: an
|
|
231
|
+
error that escapes an `action` discards its optimistic writes and rejects the
|
|
232
|
+
promise the call returned — no boundary sees it, and uncaught it is an unhandled
|
|
233
|
+
rejection with the UI silently reverted. Catch expected failures inside the action
|
|
234
|
+
(per-row: an errors map the projection folds in, which survives the optimistic
|
|
235
|
+
overlay; otherwise a toast), and `.catch` the call in the handler for the rest.
|
|
195
236
|
Error monitoring is a hook, not a side effect in the fallback: the boundary-caught
|
|
196
237
|
failures no global handler sees reach `configureClientErrors({ onError })` from
|
|
197
|
-
`solid-js` (or `render(
|
|
238
|
+
`solid-js` (or per root: `render(() => <App />, root, undefined, { onError })` — options
|
|
239
|
+
are the 4th argument, the 3rd is `init` — and `hydrate(fn, root, { onError })`) and `configureServerErrors({ onError })`
|
|
198
240
|
from `@solidjs/web` — do not call `captureException` inside `<Errored fallback>`.
|
|
199
241
|
The server hook's return value replaces the sanitized error on the wire: return
|
|
200
242
|
nothing or a reference id, never the error itself (message/stack/secrets would
|
|
201
243
|
reach the browser).
|
|
202
244
|
Making a client store "real" is additive: same setters, wrap mutations in
|
|
203
|
-
`action`, swap
|
|
245
|
+
`action`, swap in the **function form** `createOptimisticStore(() => api.list(), [])`
|
|
246
|
+
(with `refresh` after the `yield`) and a file of server functions — do
|
|
204
247
|
not rewrite `App.tsx` with loading/error branches.
|
|
205
248
|
No request counters or `AbortController`s for dedup/cancellation — superseded
|
|
206
249
|
answers drop automatically. Throw unusable responses; never `{ success: false }`
|
|
@@ -214,6 +257,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
214
257
|
On a derived store, `refresh(store)` re-runs the derivation (`fn`); refreshing a
|
|
215
258
|
plain signal accessor is a no-op. `refresh` returns a promise for the target's next
|
|
216
259
|
settled state (`await refresh(user)`, or `yield refresh(x)` inside an action).
|
|
260
|
+
Polling is `onSettled(() => { const t = setInterval(() => refresh(stats), 30_000); return () => clearInterval(t); })`.
|
|
261
|
+
Two components that each create the same memo make two requests (no automatic
|
|
262
|
+
dedup) — create it once and pass it down or through context (router: `query`).
|
|
217
263
|
These are three different APIs: core `refresh(source)` reruns a reactive source;
|
|
218
264
|
router `revalidate(getUser.key)` invalidates the query cache; server-function
|
|
219
265
|
`return reload({ revalidate: "todos" })` asks the integration to refresh cached
|
|
@@ -224,16 +270,27 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
224
270
|
apply `reconcile(fresh, "id")(draft.slot)` inside a draft setter. Plain wholesale
|
|
225
271
|
assignment (`draft.todos = fresh`) renders correctly but notifies every subscriber
|
|
226
272
|
under the path and destroys row identity — same class of problem as unkeyed `<For>`.
|
|
273
|
+
`createProjection` returns the store itself, not a tuple (`const [rows] = createProjection(…)`
|
|
274
|
+
type-checks and yields row 0). `reconcile` onto a single-object store whose `id`
|
|
275
|
+
changes throws in dev (`Cannot reconcile states with different identity`) — switch records
|
|
276
|
+
with the function form (`createStore(() => getUser(id()), seed)`) or a returned object.
|
|
227
277
|
21. **Solid inputs do not rewind the DOM.** `value={v()}` writes only when `v` changes; a
|
|
228
278
|
React-style controlled input that ignores invalid keystrokes silently breaks. To reject
|
|
229
279
|
input, write back explicitly:
|
|
230
280
|
`onInput={(e) => { if (valid(e.currentTarget.value)) setV(e.currentTarget.value); else e.currentTarget.value = v(); }}`.
|
|
231
281
|
22. Miscellaneous defaults: SSR-stable element ids come from `createUniqueId()` (never
|
|
232
282
|
`Math.random()` or hardcoded duplicates); pass store data to `structuredClone` /
|
|
233
|
-
`postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity)
|
|
283
|
+
`postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity) — a
|
|
284
|
+
view, not a copy: unchanged subtrees are the store's own objects, so mutating it (a
|
|
285
|
+
library's `arr.sort()`) silently changes the store; copy with `structuredClone(snapshot(store))`; render
|
|
234
286
|
modals/tooltips/overlays through `<Portal>` from `@solidjs/web`; mutations whose writes
|
|
235
287
|
cross an async gap default to `action` + `createOptimistic`/`createOptimisticStore` —
|
|
236
|
-
the sync mutation *is* the prediction (an overlay, discarded on settle)
|
|
288
|
+
the sync mutation *is* the prediction (an overlay, discarded on settle). Durable data
|
|
289
|
+
therefore lives in the function form (`createOptimisticStore(() => api.list(), [])`,
|
|
290
|
+
or one that reads a separate `createStore`); the value form
|
|
291
|
+
`createOptimisticStore([])` / `createOptimistic(false)` is a pure overlay with no
|
|
292
|
+
durable layer — action writes vanish on settle (a saved row disappears) and writes
|
|
293
|
+
outside an action do not stick — so use it only for in-flight flags. Write the
|
|
237
294
|
body as a generator (`function*` / `async function*`) suspending on `yield`, never a
|
|
238
295
|
plain `async` function (`solid2-kit check` flags that). After a bare `await`, put a
|
|
239
296
|
bare `yield` before the next write **and** before anything that creates a reader
|
|
@@ -256,7 +313,12 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
256
313
|
after touching `package.json` / tsconfig / root configs; it fails on React and Solid 1.x
|
|
257
314
|
wiring. Server vs browser:
|
|
258
315
|
`isServer` / `isDev` from `@solidjs/web`, or `clientOnly(() => import("./Widget"))` for
|
|
259
|
-
browser-only components — never `typeof window` as the SSR boundary.
|
|
316
|
+
browser-only components — never `typeof window` as the SSR boundary. `isServer` picks
|
|
317
|
+
code paths, never markup: a JSX branch on it (or on `Date.now()` / `Math.random()`) is
|
|
318
|
+
a hydration mismatch. On the server the component body and memo/effect *computes*
|
|
319
|
+
run; effect `apply`, `onSettled`, and handlers do not. A `clientOnly` placeholder is
|
|
320
|
+
its `fallback` prop (`<Chart fallback={<Skeleton />} />`, rendered by SSR), not a
|
|
321
|
+
`<Loading>` around it; named exports take `{ export: "Chart" }`. Default
|
|
260
322
|
`clientOnly` starts loading at declaration; `{ lazy: true }` defers until first
|
|
261
323
|
render. `NoHydration` / `Hydration` split hydration *ownership*; they do not choose
|
|
262
324
|
visible content — `clientOnly` is for components that must never run on the server.
|
|
@@ -282,7 +344,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
282
344
|
fallback body), not event-time mutations. `JSON.stringify(store)`
|
|
283
345
|
/ `structuredClone(store)` without `snapshot(store)` can throw or leak proxies. Async SSR
|
|
284
346
|
without `<Loading>` *works* but blocks the HTML shell until every read settles — wrap
|
|
285
|
-
reads, or stream with `renderToStream`.
|
|
347
|
+
reads, or stream with `renderToStream`. A read that must be in the first HTML (SEO,
|
|
348
|
+
link previews) takes `{ deferStream: true }` on its memo; keep the boundary. `pipe` / `pipeTo` / `readable` each consume
|
|
286
349
|
a stream render — use exactly one. `createRoot` is for tests, libraries, and
|
|
287
350
|
non-render entry points; inside a component let `render` / the component owner
|
|
288
351
|
own the scope. Async reads inside `<Portal>` start on the client — hoist the
|
|
@@ -309,29 +372,47 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
309
372
|
`revalidate(getUser.key)`. Do not wrap mutations in `query` (an undeclared server
|
|
310
373
|
function becomes GET). `Router.paths` is not the current location (`useLocation` /
|
|
311
374
|
`useParams`); search params via `useSearchParams` (merging setter; typed with a route
|
|
312
|
-
`search` schema + path node). A route `preload`
|
|
375
|
+
`search` schema + path node). A route `preload` *starts* reads and returns nothing —
|
|
376
|
+
`preload: ({ params }) => void getProduct(params.id)` (wrap the route in `defineRoute`
|
|
377
|
+
to type `params`) — and the page reads the same query in a memo:
|
|
378
|
+
`createMemo(() => getProduct(props.params.id))`. `props.data` is whatever `preload`
|
|
379
|
+
returned when the route first matched: it does not follow param changes (and an async
|
|
380
|
+
`preload` makes it a Promise), so return a value only for data that stays fixed while
|
|
381
|
+
the route is matched. One router instance —
|
|
313
382
|
Solid Router does not support nested `<Router>` / nested `createRouter`.
|
|
314
383
|
In-app navigation is `useNavigate` or `<a href={Router.paths...}>`, not
|
|
315
384
|
`window.location` / `history.pushState`. Link state is automatic
|
|
316
385
|
(`aria-current` / `data-active` / `data-pending` + CSS; `useLinkState` /
|
|
317
386
|
`useIsRouting` in JSX) — never hand-rolled `location.pathname` comparisons. Trusted identity is
|
|
318
|
-
`getRequestEvent()`, never a caller-supplied user id. Unscripted
|
|
319
|
-
|
|
320
|
-
|
|
387
|
+
`getRequestEvent()`, never a caller-supplied user id. Unscripted (no-JS) POST
|
|
388
|
+
forms use a router `action` over a `(form: FormData)` server function (the post
|
|
389
|
+
arrives as one `FormData`; other signatures make `action={save}` TS2322) —
|
|
390
|
+
`<form action={save} method="post">` — not the
|
|
391
|
+
function's `.url` and not a hand-built `/_server/` path: a bare `"use server"`
|
|
392
|
+
function has no typed `.url` (TS2339). `.url` is typed only on `GET()` / `live()`
|
|
393
|
+
references, for `method="get"` search forms. Client history adapters do not select the SSR URL — pass
|
|
321
394
|
`<Router url={request.url}>` (or rely on the request event). Secrets belong in
|
|
322
395
|
`virtual:env/server`, never the `client` env map / `import.meta.env`. Document head is
|
|
323
396
|
`<Title>` / `<Meta>` from `@solidjs/meta` with **no** `MetaProvider`. A static
|
|
324
397
|
`<title>` in the document shell is the fallback; do not hardcode other tags
|
|
325
398
|
(`<meta name="description">`) that Solid Meta should manage.
|
|
326
|
-
|
|
327
|
-
`Response.json` — a raw `Response` is HTTP-handler control
|
|
328
|
-
callers should receive the value.
|
|
399
|
+
Success metadata is `return respond(value, { status: 201, headers })` from a
|
|
400
|
+
`"use server"` function, not `Response.json` — a raw `Response` is HTTP-handler control
|
|
401
|
+
flow; scripted callers should receive the value. A *returned* `respond()` resolves the
|
|
402
|
+
call whatever its status, so a failure is `throw respond(body, { status: 400 })`. Production sanitizes every thrown value that
|
|
329
403
|
crosses the wire — server-function throws **and** SSR render errors (an `<Errored>`
|
|
330
404
|
fallback printing `err().message` shows `"Internal Server Error"` in a production
|
|
331
405
|
server render) — so intentional client-facing failures are `markSafeError(...)` or
|
|
332
406
|
`throw respond(body, { status })`. Reads are function calls too (`await getUser(id)`
|
|
333
|
-
on a `GET()`-declared read; the method mirrors the operation).
|
|
334
|
-
|
|
407
|
+
on a `GET()`-declared read; the method mirrors the operation). `redirect()` /
|
|
408
|
+
`reload()` are signals for an integration: Solid Router's `action` / `query` and the
|
|
409
|
+
no-JS form post apply them. A direct caller — a core `action`, a handler, a plain
|
|
410
|
+
memo — never gets your data: it receives the raw `Response` as the resolved value
|
|
411
|
+
(typed as your data, no navigation); only when a mounted router's single-flight
|
|
412
|
+
hook intercepts a direct POST call does it navigate, and the call then resolves to
|
|
413
|
+
`null`. So guards `throw redirect("/sign-in")` only when router `action` /
|
|
414
|
+
`query` call the function; without the router, `throw respond(null, { status: 401 })`
|
|
415
|
+
(the call rejects) or return a value and navigate in the caller. `redirect()` /
|
|
335
416
|
`respond()` take `revalidate` like `reload()`. Cache GET reads with
|
|
336
417
|
`Cache-Control` on that response (the default is `no-store`), not a hand-rolled
|
|
337
418
|
Solid cache. HTTP does not enforce TypeScript —
|
|
@@ -346,14 +427,18 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
346
427
|
display). Do not write subscribe/unsubscribe in the component around `live()`.
|
|
347
428
|
When the app has a server bundle (`ssr` or server functions), production is
|
|
348
429
|
`handleRequest(request)` from the built `dist/server/server.js` (or its
|
|
349
|
-
Fetchable `fetch`); platform Vite plugins adopt that handler
|
|
350
|
-
|
|
430
|
+
Fetchable `fetch`); platform Vite plugins adopt that handler. Node: set
|
|
431
|
+
`start: { node: true }` and run `node dist/server/node.js` (serves `dist/client`,
|
|
432
|
+
reads `PORT` / `HOST`; its exported `listener` mounts in an existing Express /
|
|
433
|
+
Fastify server) — `server.js` is only the handler module. Client-only start mode is static `dist/client` —
|
|
351
434
|
there is no handler to wrap. Request middleware is
|
|
352
435
|
`start: { middleware: "./src/middleware.ts" }`, not Express `app.use`. Do not return
|
|
353
436
|
components from `"use server"` unless the project already enabled the
|
|
354
|
-
experimental `serverFunctions.components` flag.
|
|
355
|
-
|
|
356
|
-
|
|
437
|
+
experimental `serverFunctions.components` flag. Server-function arguments are JSON:
|
|
438
|
+
send `Date` / `Map` / `Set` as ISO strings / arrays (they throw otherwise; one `File` /
|
|
439
|
+
`Blob` / `FormData` argument travels natively). `enableRichArguments()` from
|
|
440
|
+
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that,
|
|
441
|
+
but in rc.9 importing it fails `vite build` (`"./client" is not exported`). Declare live reads as `live(GET(fn))` —
|
|
357
442
|
`live()` outermost. A plain `async function*` server function is an event stream on
|
|
358
443
|
one connection (no reconnect when it drops); `live()` is one value that changes over
|
|
359
444
|
time and reconnects. `live()` connection state is
|
|
@@ -384,7 +469,7 @@ directory (default `src/`).
|
|
|
384
469
|
| `solid/event-handlers` | still valid | rule 6 |
|
|
385
470
|
| `solid/jsx-no-undef`, `jsx-no-duplicate-props` | covered by TypeScript | project typecheck |
|
|
386
471
|
| `solid/prefer-classlist` | obsolete — `classList` removed | `solid2-kit check` bans `classList` |
|
|
387
|
-
| `solid/no-unknown-namespaces` | obsolete — `on:`/`use:`/`attr:` removed in 2.0 | banned-API table + `solid2-kit check` |
|
|
472
|
+
| `solid/no-unknown-namespaces` | obsolete — `on:`/`use:`/`attr:` removed in 2.0 (`prop:` remains) | banned-API table + `solid2-kit check` |
|
|
388
473
|
| `solid/no-proxy-apis` | obsolete — Solid 2 requires Proxy | not carried |
|
|
389
474
|
|
|
390
475
|
## Banned Solid 1.x APIs (Solid 2 replacements)
|
|
@@ -392,18 +477,18 @@ directory (default `src/`).
|
|
|
392
477
|
| Never write (Solid 1.x) | Write instead (Solid 2) |
|
|
393
478
|
|---|---|
|
|
394
479
|
| `import ... from "solid-js/store"` or `"solid-js/web"` | stores/`merge`/`omit` from `"solid-js"`; `render`/`hydrate`/`Portal`/`Dynamic` from `"@solidjs/web"` |
|
|
395
|
-
| `import type { JSX } from "solid-js"`
|
|
480
|
+
| `import type { JSX } from "solid-js"` | `solid-js` exports no `JSX` namespace: markup types are `JSX.Element` from `"@solidjs/web"` (with `JSX.IntrinsicElements`, `JSX.CSSProperties`) or `Element` from `"solid-js"` — import the latter as `type Element as SolidElement` where the DOM `Element` is also used, or it shadows it |
|
|
396
481
|
| `createResource` | async `createMemo` + `<Loading>`/`<Errored>`; `refresh()`, `latest()`, `isPending()` |
|
|
397
|
-
| `createEffect(fn)` (one arg), `on(...)` | `createEffect(compute, apply)`; deps belong in `compute` |
|
|
398
|
-
| `onMount` | `onSettled`
|
|
482
|
+
| `createEffect(fn)` (one arg), `on(...)`, `createTrackedEffect` (deprecated in 2.0) | `createEffect(compute, apply)`; deps belong in `compute`; one-time DOM work is `onSettled` |
|
|
483
|
+
| `onMount` | `onSettled(() => { ...; return cleanup })` — return the cleanup; `onCleanup`, memo/effect creation, and `flush()` throw inside the callback |
|
|
399
484
|
| `batch(...)` | delete it — writes auto-batch; `flush()` only to observe synchronously |
|
|
400
485
|
| `<Suspense>`, `<ErrorBoundary>`, `<SuspenseList>` | `<Loading>`, `<Errored>` (fallback gets an error *accessor*), `<Reveal>` |
|
|
401
486
|
| `<Index>` | `<For keyed={false}>` |
|
|
402
487
|
| `<Ctx.Provider value={...}>` | `<Ctx value={...}>` |
|
|
403
|
-
| `setState("a", "b", value)` path setters, `produce(...)` | draft setter: `setState(draft => { ... })` |
|
|
488
|
+
| `setState("a", "b", value)` path setters, `setState({ a })` merges, `produce(...)` | draft setter: `setState(draft => { ... })` (the others throw `fn is not a function`) |
|
|
404
489
|
| `mergeProps` / `splitProps` / `unwrap` | `merge` / `omit` / `snapshot` |
|
|
405
490
|
| `classList={...}` | `class` object/array form |
|
|
406
|
-
| `createMutable`, `modifyMutable` | `createStore` + draft setters |
|
|
491
|
+
| `createMutable`, `modifyMutable` | `createStore` + draft setters (direct writes to the store are silently ignored) |
|
|
407
492
|
| `createComputed`, `createSelector`, `createDeferred` | `createMemo`, `createProjection`, external scheduling |
|
|
408
493
|
| `startTransition`, `useTransition` | automatic held updates + `isPending` |
|
|
409
494
|
| `onError` / `catchError` | `<Errored>` or effect bundle `{ effect, error }` |
|
|
@@ -413,19 +498,19 @@ directory (default `src/`).
|
|
|
413
498
|
| `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
|
|
414
499
|
| `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
|
|
415
500
|
| JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` / `<FileRoutes>` | `createRouter({ routes })`, `fileRoutes(pageRoutes)` (`fileRoutes` from `@solidjs/router/fs`, `pageRoutes` from `virtual:file-routes`), plain `<a href={Router.paths...}>`, `hashHistory()`; link state via automatic `aria-current` / `data-active` / `data-pending` + CSS (`useLinkState` in JSX) |
|
|
501
|
+
| `useCurrentMatches`, `MemoryRouter` / `createMemoryHistory`, `redirect` / `reload` from `@solidjs/router`, `indexArray` | `useRouteMatches`, `createRouter({ routes, history: memoryHistory("/x") })`, `redirect` / `reload` from `@solidjs/web`, `mapArray(list, fn, { keyed: false })` |
|
|
416
502
|
| `createAsync` / `createAsyncStore` / `useSubmission` / router `json()` / `cache()` | `createMemo(() => getUser(id()))`, `useSubmissions`, `respond()` from `@solidjs/web`, `query` |
|
|
417
503
|
| `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
|
|
418
504
|
| `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |
|
|
419
505
|
| `<MetaProvider>` | no provider — render `<Title>` / `<Meta>` / `<Link>` from `@solidjs/meta` anywhere |
|
|
420
|
-
| `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` | `ref` callbacks, camelCase event props, standard attributes, keep values reactive |
|
|
506
|
+
| `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` | `ref` callbacks, camelCase event props, standard attributes, keep values reactive; `prop:` remains for DOM properties (`prop:indeterminate`, objects/arrays on custom elements — plain props are attributes and stringify) |
|
|
421
507
|
| `resource.loading` / `resource.error` | `<Loading>` boundary / `<Errored>` boundary |
|
|
422
508
|
|
|
423
509
|
When unsure about any API, verify against the official Solid 2.0 docs — fetchable URLs are
|
|
424
510
|
listed in `references/official-docs.md` next to the `solid-2` skill. Do not guess from
|
|
425
511
|
Solid 1.x or React memory.
|
|
426
512
|
|
|
427
|
-
`
|
|
428
|
-
use two-phase `createEffect` unless a single tracked callback is required. `storePath`
|
|
513
|
+
`createRenderEffect` exists in Solid 2 but is not a default. `storePath`
|
|
429
514
|
(the 1.x path-setter migration helper) is no longer exported from `solid-js` as of
|
|
430
515
|
2.0.0-rc.9 — convert path setters to draft setters instead of importing it from
|
|
431
516
|
`@solidjs/signals`.
|