solid2-agent-kit 0.8.0 → 0.9.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/bin/solid2-kit.mjs +5 -1
- package/files/commands/solid2-review.md +14 -2
- package/files/shared/agents-section.md +1 -1
- package/files/shared/rules-body.md +61 -14
- package/files/skills/solid-2/SKILL.md +276 -51
- package/files/skills/solid-2/references/official-docs.md +10 -0
- package/package.json +1 -1
package/bin/solid2-kit.mjs
CHANGED
|
@@ -1059,7 +1059,11 @@ function doctorFindings(target) {
|
|
|
1059
1059
|
for (const guidance of GUIDANCE_FILES) {
|
|
1060
1060
|
const path = join(target, guidance);
|
|
1061
1061
|
if (!existsSync(path)) continue;
|
|
1062
|
-
|
|
1062
|
+
// The version must end on a word character so the sentence punctuation
|
|
1063
|
+
// that follows the marker ("… v0.8.0. Do not edit …", "…v0.8.0**.") is
|
|
1064
|
+
// not captured — with `[\w.-]*` alone the trailing period made every
|
|
1065
|
+
// freshly synced file compare as "0.8.0." !== "0.8.0" (always stale).
|
|
1066
|
+
const installed = readFileSync(path, 'utf8').match(/solid2-agent-kit v(\d(?:[\w.-]*\w)?)/)?.[1];
|
|
1063
1067
|
if (installed && installed !== VERSION) {
|
|
1064
1068
|
report(
|
|
1065
1069
|
'stale-guidance',
|
|
@@ -17,8 +17,20 @@ Steps:
|
|
|
17
17
|
- async computations reading reactive inputs **after** the first `await`;
|
|
18
18
|
- effects that copy state instead of deriving it, or reading stores in the apply phase;
|
|
19
19
|
- `<For>` over server/refetched rows without a stable-id key function;
|
|
20
|
-
- `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or
|
|
21
|
-
|
|
20
|
+
- `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or
|
|
21
|
+
`const u = user()` extracted then passed (a real parent-side read). Passing
|
|
22
|
+
`user={user()}` is the colorless form — do not "fix" it into accessors or
|
|
23
|
+
`Promise<User>` props;
|
|
24
|
+
- `latest(selectedId)` as the default highlight (hold is the default);
|
|
25
|
+
`isPending` treated as a global spinner instead of a per-expression question;
|
|
26
|
+
- treating `<Errored>` as a terminal ErrorBoundary, or routing per-row mutation
|
|
27
|
+
failures through it (those belong in the action / a projection-folded map);
|
|
28
|
+
- nested fetches assumed to waterfall, or `<Loading>` lifted along with a lifted fetch;
|
|
29
|
+
- rewriting App with loading/error branches, or snapshot/restore, when wrapping
|
|
30
|
+
a client store in server functions; disabling optimistic rows until ack;
|
|
31
|
+
- tRPC / type-gen around `"use server"`, a client `fetch` after a mutation,
|
|
32
|
+
refetch in `hydrate`/`onSettled`, a custom Worker/Express adapter, or
|
|
33
|
+
delaying `renderToStream` for visual order (`<Reveal>` owns display);
|
|
22
34
|
- store updates that rebuild objects/arrays instead of mutating the draft or reconciling;
|
|
23
35
|
- context values passed as snapshots instead of accessors/setters/stores;
|
|
24
36
|
- components with conditional/early returns on reactive values.
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
- After editing TSX, run the `solid2-kit check` mechanical gate — it fails on React/Solid 1.x tokens and props destructuring. Use the project's `lint:solid` script if one exists; otherwise `npx solid2-agent-kit check` (with the kit as a devDependency) or `npx github:lightsound/solid2-agent-kit check`. An edit-time hook may also run this gate automatically after each file edit — treat its findings as errors to fix immediately, not warnings. Do not install `eslint-plugin-solid` (built for Solid 1.x; misreads Solid 2 idioms).
|
|
6
6
|
- After editing `package.json`, `tsconfig*.json`, or root config files (Vite, ESLint), run `solid2-kit doctor` — it fails on React/Solid 1.x wiring: `react`/`vite-plugin-solid`/`eslint-plugin-solid`/SolidStart dependencies, 1.x `solid-js` ranges, and `jsx`/`jsxImportSource` values other than `preserve`/`@solidjs/web`.
|
|
7
7
|
- The kit's guidance and gate files (`.cursor/rules/solid-2.mdc`, the `solid-2` skill directories, hook entries, managed blocks in this file and `CLAUDE.md`) are guardrails, not obstacles. Never edit, weaken, bypass, or delete them to make a failure go away — fix the flagged code. If a guardrail seems wrong, stop and tell the user.
|
|
8
|
-
- Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup`. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace — children/return types are `Element` from `solid-js`; DOM `JSX` types come from `@solidjs/web`. Reset-a-signal-on-change is a writable derivation (`createSignal(() => { source(); return initial; })`), never an effect calling the setter. Client mode mounts with `render()` into an empty body — no hydration mismatch exists, so never defer initial values to post-mount React-style (that can overwrite persisted state).
|
|
8
|
+
- Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass async values as values (`user={user()}`) — JSX props are lazy; types stay `User`, not `Promise<User>`. `<Loading>` wraps the read, not the memo. Nested fetches run in parallel (components do not suspend). `isPending` is a per-expression question (`isPending(() => props.story)` / `isPending(selectedId)`), not a global spinner; `latest(selectedId)` is opt-in so highlight and content stay consistent by default. `<Errored>` heals when the source succeeds. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup`. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace — children/return types are `Element` from `solid-js`; DOM `JSX` types come from `@solidjs/web`. Reset-a-signal-on-change is a writable derivation (`createSignal(() => { source(); return initial; })`), never an effect calling the setter. Client mode mounts with `render()` into an empty body — no hydration mismatch exists, so never defer initial values to post-mount React-style (that can overwrite persisted state). Client → server is additive (same setters + `action` + server functions): do not rewrite App with loading/error branches, disable optimistic rows until ack, invent tRPC, or refetch in `hydrate`/`onSettled`. The graph continues across the wire (`live()` owns the client connection; `<Reveal>` not a delayed stream). A server bundle uses `handleRequest` / Fetchable `fetch`; client-only start is static `dist/client`.
|
|
@@ -75,20 +75,35 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
75
75
|
array from the setter (`setTodos((t) => t.filter(...))`) *renders* but is unkeyed — same
|
|
76
76
|
identity loss as wholesale assignment; mutate the draft or `reconcile`. Stores are
|
|
77
77
|
**property reads** (`todos.length`, `todo.title`), not accessors — never `todos()`.
|
|
78
|
-
10. **Async data is an async computation**: `createMemo(async () => ...)`
|
|
79
|
-
`<Loading>` / `<Errored
|
|
78
|
+
10. **Async data is an async computation**: `createMemo(async () => ...)` (or
|
|
79
|
+
`createMemo(() => fetchUser(id()))`) read under `<Loading>` / `<Errored>`.
|
|
80
|
+
**Fetch high, block low** — creating/reading are like sync; consuming (await)
|
|
81
|
+
and blocking (the boundary) are independent. Create the memo early, wrap only
|
|
82
|
+
the read with `<Loading>`. Do not glue the boundary to the memo, and do not
|
|
83
|
+
retarget intermediate props to `Promise<User>` / `Accessor<User>` — types stay
|
|
84
|
+
`User`. Nested trees do not suspend: they fetch in parallel; a real waterfall
|
|
85
|
+
looks like `fetchB(a().id)`. JSX component props are lazy (passing isn't reading):
|
|
86
|
+
`<Child user={user()} />` then `{props.user.name}` under the child's
|
|
87
|
+
`<Loading>` is the colorless form. `const u = user()` at the parent *is* a
|
|
88
|
+
read and throws/snapshots there. Pass the memo itself only when the child
|
|
89
|
+
must `refresh()` that source — `isPending(() => props.user)` works on the
|
|
90
|
+
value. Read every reactive input **before the first
|
|
80
91
|
`await`** — post-`await` reads do not subscribe, and in production the computation can
|
|
81
92
|
sit pending with no retry. No `useEffect` + `setState` fetching, no `createResource`.
|
|
82
93
|
`<Loading>` wraps the data slot, not page chrome. After first paint it keeps content
|
|
83
94
|
during refetch (`isPending` for the indicator). Use `on={id()}` (the *value*, not the
|
|
84
|
-
accessor) only when that identity change should show the fallback again.
|
|
85
|
-
memo to a child as the **accessor** (`user={user}`), and read `props.user()` under that
|
|
86
|
-
child's `<Loading>` — `user={user()}` throws not-ready at the parent, so the child's
|
|
87
|
-
boundary never sees it. Do not start `fetch` (or any request) at component-body top
|
|
95
|
+
accessor) only when that identity change should show the fallback again. Do not start `fetch` (or any request) at component-body top
|
|
88
96
|
level — that runs once at mount and is not a reactive source. Do not `try/catch` `NotReadyError` around a read, and do not
|
|
89
97
|
use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
|
|
90
|
-
`<Loading>`. `{latest(() => x())}` is a preview, not the visible answer.
|
|
91
|
-
|
|
98
|
+
`<Loading>`. `{latest(() => x())}` is a preview, not the visible answer.
|
|
99
|
+
Default navigation holds: keep `selectedId()` so highlight and content stay
|
|
100
|
+
consistent; `latest(selectedId)` only when the highlight should move first.
|
|
101
|
+
Pair `class={{ pending: isPending(selectedId) }}` with a short CSS
|
|
102
|
+
`transition-delay` so fast swaps do not flash.
|
|
103
|
+
`isPending` is a per-expression question, not a global spinner: ask it of the
|
|
104
|
+
async source, a derived prop (`isPending(() => props.story)`), or the write
|
|
105
|
+
(`isPending(selectedId)`), even when the fetch lives in a child. It is
|
|
106
|
+
the *refetch* indicator after a settled answer exists — do not use it as the
|
|
92
107
|
first-load spinner (`<Loading>` owns that). Pass the accessor: `isPending(user)`,
|
|
93
108
|
not `isPending(user())` (that evaluates the read before `isPending` runs).
|
|
94
109
|
"Prop with local edits" = writable derivation:
|
|
@@ -143,8 +158,15 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
143
158
|
integration layers. Model async as a computation (return a promise or async iterable
|
|
144
159
|
from a memo/store) and let the framework own the states: first load = `<Loading>`
|
|
145
160
|
fallback; refetch indicator = `isPending()`; errors = thrown into the graph and caught
|
|
146
|
-
by `<Errored>`; speculative value = `latest()`.
|
|
147
|
-
|
|
161
|
+
by `<Errored>`; speculative value = `latest()`. `<Errored>` is graph status, not a
|
|
162
|
+
terminal React ErrorBoundary: it heals when the source succeeds again (refresh,
|
|
163
|
+
live reconnect, input change). `reset` retries the collected *sources*, not a UI
|
|
164
|
+
remount. Per-row mutation failures belong in an errors map the projection folds
|
|
165
|
+
in (survives the optimistic overlay), not in `<Errored>` around the whole list.
|
|
166
|
+
Making a client store "real" is additive: same setters, wrap mutations in
|
|
167
|
+
`action`, swap `createOptimisticStore` and a file of server functions — do
|
|
168
|
+
not rewrite `App.tsx` with loading/error branches.
|
|
169
|
+
Mechanically enforced by `solid2-kit check` (loading-signal naming, zero-arg-call undefined checks).
|
|
148
170
|
19. **Refetch is `refresh(source)`** — rarely needed, but when it is, never fake it with a
|
|
149
171
|
counter/version signal read inside the computation. Input-driven refetch is automatic
|
|
150
172
|
(tracked inputs re-run the computation) and subscriptions push. Legit uses: after a
|
|
@@ -169,7 +191,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
169
191
|
`postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity); render
|
|
170
192
|
modals/tooltips/overlays through `<Portal>` from `@solidjs/web`; mutations whose writes
|
|
171
193
|
cross an async gap default to `action` + `createOptimistic`/`createOptimisticStore` —
|
|
172
|
-
|
|
194
|
+
the sync mutation *is* the prediction (an overlay, discarded on settle); put
|
|
195
|
+
`pending` on the record rather than a second copy of state. Do not snapshot
|
|
196
|
+
and restore, disable optimistic rows until ack, mutex rapid clicks, or freeze
|
|
197
|
+
unrelated writes because one action is in flight; failed-action replay is
|
|
198
|
+
optional. Except reactive clients (e.g. Convex) whose subscriptions already push authoritative
|
|
173
199
|
state after mutations. Compiler: `"jsxImportSource": "@solidjs/web"` (not `"solid-js"`);
|
|
174
200
|
Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`) — run `solid2-kit doctor`
|
|
175
201
|
after touching `package.json` / tsconfig / root configs; it fails on React and Solid 1.x
|
|
@@ -230,11 +256,32 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
230
256
|
`<Title>` / `<Meta>` from `@solidjs/meta` with **no** `MetaProvider`. A static
|
|
231
257
|
`<title>` in the document shell is the fallback; do not hardcode other tags
|
|
232
258
|
(`<meta name="description">`) that Solid Meta should manage.
|
|
233
|
-
Return `respond(value, { status })` from a `"use server"` function, not
|
|
259
|
+
Return `respond(value, { status, headers })` from a `"use server"` function, not
|
|
234
260
|
`Response.json` — a raw `Response` is HTTP-handler control flow; scripted
|
|
235
|
-
callers should receive the value.
|
|
261
|
+
callers should receive the value. Cache GET reads with `Cache-Control` on that
|
|
262
|
+
response, not a hand-rolled Solid cache. HTTP does not enforce TypeScript —
|
|
263
|
+
validate inside the function; do not invent tRPC / RPC type-gen.
|
|
264
|
+
Locals only the `"use server"` body reads (db, secrets) stay off the client.
|
|
265
|
+
Do not add an API-route file just to wrap a database call — `"use server"`
|
|
266
|
+
is the RPC. During SSR the call is in-process (no HTTP). After a mutation, `reload` /
|
|
267
|
+
`revalidate` / single-flight — never a client `fetch` or an `onSettled` /
|
|
268
|
+
`hydrate` refetch to "refresh the page". In-flight Promises serialize as
|
|
269
|
+
Promises; `live()` embeds the first value in HTML and continues the stream on
|
|
270
|
+
the client. Do not delay `renderToStream` for visual order (`<Reveal>` owns
|
|
271
|
+
display). Do not write subscribe/unsubscribe in the component around `live()`.
|
|
272
|
+
When the app has a server bundle (`ssr` or server functions), production is
|
|
273
|
+
`handleRequest(request)` from the built `dist/server/server.js` (or its
|
|
274
|
+
Fetchable `fetch`); platform Vite plugins adopt that handler (Node: the
|
|
275
|
+
template `server.js`). Client-only start mode is static `dist/client` —
|
|
276
|
+
there is no handler to wrap. Request middleware is
|
|
277
|
+
`start: { middleware: "./src/middleware.ts" }`, not Express `app.use`. Do not return
|
|
278
|
+
components from `"use server"` unless the project already enabled the
|
|
279
|
+
experimental `serverFunctions.components` flag. JSON-encodable server-function arguments
|
|
236
280
|
only, unless `enableRichArguments()` was called once in the client entry
|
|
237
|
-
(`Date` / `Map` / `Set` throw without it).
|
|
281
|
+
(`Date` / `Map` / `Set` throw without it). `live()` connection state is
|
|
282
|
+
`source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`), never a
|
|
283
|
+
field in the yielded value. Sibling `<Loading>` reveal order is `<Reveal>`
|
|
284
|
+
(`collapsed` suppresses tail skeletons under sequential order).
|
|
238
285
|
|
|
239
286
|
## Lint heritage: eslint-plugin-solid (Solid 1.x) intents carried into this file
|
|
240
287
|
|
|
@@ -70,6 +70,8 @@ Most React `useEffect` code should NOT become `createEffect`:
|
|
|
70
70
|
| Named export from a lazy module | `lazy(() => import("./pages"), { export: "About" })` |
|
|
71
71
|
| Pick a component/tag from reactive state | `dynamic(() => ...)` from `@solidjs/web` (stable identity; prefer over `<Dynamic>`) |
|
|
72
72
|
| Overlay / modal | `<Portal>` — hoist async reads *above* the portal (reads inside start on the client) |
|
|
73
|
+
| 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
|
+
| 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)`) |
|
|
73
75
|
| Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render) |
|
|
74
76
|
| Server vs browser branch | `isServer` / `isDev` from `@solidjs/web` (build-time constants), not `typeof window` |
|
|
75
77
|
| SSR-stable `id` / `for` / `aria-*` pairing | `createUniqueId()` |
|
|
@@ -245,44 +247,106 @@ const [signInError, setSignInError] = createSignal<string | null>(() => {
|
|
|
245
247
|
});
|
|
246
248
|
```
|
|
247
249
|
|
|
248
|
-
### Async data
|
|
250
|
+
### Async data — fetch high, block low
|
|
251
|
+
|
|
252
|
+
Creating and reading an async value are the same as sync. **Consuming** (the
|
|
253
|
+
await) is a DX choice; **blocking** (the boundary) is a UX choice. Solid
|
|
254
|
+
unwelds them: where you create the memo is performance, where `<Loading>`
|
|
255
|
+
wraps the *read* is design, and neither is an architecture decision. JSX
|
|
256
|
+
component props are lazy (**passing isn't reading**), so intermediates do
|
|
257
|
+
not wait. Nested components mount immediately — they do not suspend — so
|
|
258
|
+
their fetches run in parallel. Making a value remote should touch the memo
|
|
259
|
+
and the boundary, not every file on the path, and should not change prop
|
|
260
|
+
types to `Promise<User>` or `Accessor<User>`.
|
|
249
261
|
|
|
250
262
|
```tsx
|
|
251
|
-
import { Errored,
|
|
263
|
+
import { Errored, Loading, createMemo, createSignal, isPending } from 'solid-js';
|
|
252
264
|
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
if (!value) return [];
|
|
257
|
-
return api.search(value);
|
|
258
|
-
});
|
|
265
|
+
function App() {
|
|
266
|
+
const [selectedId, setSelectedId] = createSignal(1);
|
|
267
|
+
const story = createMemo(() => fetchStory(selectedId()));
|
|
259
268
|
|
|
260
|
-
return (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
269
|
+
return (
|
|
270
|
+
<Errored fallback={(error, reset) => <ErrorFallback error={error} reset={reset} />}>
|
|
271
|
+
<StoryList selectedId={selectedId()} onSelect={setSelectedId} />
|
|
272
|
+
<main class={{ pending: isPending(selectedId) }}>
|
|
273
|
+
<Loading fallback={<DetailSkeleton />}>
|
|
274
|
+
<StoryDetail story={story()} storyId={selectedId()} />
|
|
275
|
+
</Loading>
|
|
276
|
+
</main>
|
|
277
|
+
</Errored>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function StoryDetail(props: { story: Story; storyId: number }) {
|
|
282
|
+
const byline = createMemo(() => `${props.story.author} · ${props.story.points} points`);
|
|
283
|
+
return (
|
|
284
|
+
<article class={{ stale: isPending(() => props.story) }}>
|
|
285
|
+
<h1>{props.story.title}</h1>
|
|
286
|
+
<p>{byline()}</p>
|
|
287
|
+
<Comments storyId={props.storyId} />
|
|
288
|
+
</article>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`story()` is a `Story`, not `Story | undefined` — if the computation is running,
|
|
294
|
+
the value is there. No `?.`, no `!`. `<StoryDetail story={story()} />` is the
|
|
295
|
+
correct colorless form: the prop expression is a getter, so `StoryDetail` (and
|
|
296
|
+
every layout in between) renders immediately. The wait happens when
|
|
297
|
+
`props.story.title` is read, under that `<Loading>`.
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
// WRONG — extracting at the parent is a real read; child's <Loading> never sees it
|
|
301
|
+
const current = story();
|
|
302
|
+
return <StoryDetail story={current} />;
|
|
303
|
+
|
|
304
|
+
// WRONG — Promise / Accessor types on the path. Making story remote is not a
|
|
305
|
+
// type change for StoryLayout / StoryDetail.
|
|
306
|
+
function StoryDetail(props: { story: Promise<Story> }) { /* ... */ }
|
|
307
|
+
function StoryDetail(props: { story: Accessor<Story> }) { /* ... */ }
|
|
308
|
+
|
|
309
|
+
// Fine — pass the memo itself only when the child must refresh() that source.
|
|
310
|
+
// isPending / latest are questions: they work on the value prop too.
|
|
311
|
+
return <StoryDetail story={story} />;
|
|
267
312
|
```
|
|
268
313
|
|
|
269
|
-
- The `<Loading>` boundary must be an owner ancestor of the **read
|
|
314
|
+
- The `<Loading>` boundary must be an owner ancestor of the **read**, not of
|
|
270
315
|
where the memo was created, and not of page chrome (header/nav) that should stay mounted.
|
|
271
|
-
|
|
272
|
-
|
|
316
|
+
Lifting the fetch does **not** mean lifting the boundary — that is consume glued
|
|
317
|
+
back onto block. After first paint the
|
|
273
318
|
boundary keeps settled content during refetch; `isPending(results)` (pass the accessor,
|
|
274
|
-
not `isPending(results())`) is the
|
|
275
|
-
|
|
276
|
-
that identity should put the fallback back on screen.
|
|
319
|
+
not `isPending(results())`) is the indicator. Use `on={query()}` (the **value**, not the
|
|
320
|
+
accessor `on={query}`) only when that identity should put the fallback back on screen.
|
|
277
321
|
Do not use `isPending` as the first-load spinner.
|
|
322
|
+
- **Nesting is not a waterfall.** `Comments` above fetches in parallel with `story`
|
|
323
|
+
because it reads `props.storyId` (already known), not `props.story`. Passing
|
|
324
|
+
`storyId={props.story.id}` *would* wait on the story fetch — that is a real data
|
|
325
|
+
dependency, same as `createMemo(() => fetchAuthor(story().authorId))`. Components
|
|
326
|
+
do not suspend; they run once. Only the expressions that read wait, so a parent
|
|
327
|
+
JSX read does not delay a child that does not need that value.
|
|
328
|
+
- `isPending` is a **per-expression question**, not state and not a global spinner.
|
|
329
|
+
Ask it anywhere: on the async source (`isPending(results)`), below it
|
|
330
|
+
(`isPending(() => props.story)`), on a derived memo (`isPending(byline)`), or
|
|
331
|
+
*above* the fetch but below the write (`isPending(selectedId)`), even when the
|
|
332
|
+
fetch lives in a child. You do not need to pass the memo accessor just to ask.
|
|
333
|
+
- Default navigation **holds**. `selectedId()` and the old story stay put until
|
|
334
|
+
the new answer lands, so the highlight never points at the wrong content.
|
|
335
|
+
`latest(selectedId)` is the opt-in when the design wants the highlight to move
|
|
336
|
+
on click. Pair `class={{ pending: isPending(selectedId) }}` with a short CSS
|
|
337
|
+
`transition-delay` so fast swaps do not flash.
|
|
278
338
|
- Read every reactive input **before the first `await`** in an async computation. A read
|
|
279
339
|
after `await` does not subscribe; production can sit pending with no retry.
|
|
280
340
|
- `<Errored>` function fallbacks receive an error **accessor** and a `reset` callback:
|
|
281
|
-
`fallback={(error, reset) => ...}`.
|
|
341
|
+
`fallback={(error, reset) => ...}`. The boundary **heals** when the source succeeds
|
|
342
|
+
again (refresh, live reconnect, input change) — it is graph status, not a terminal
|
|
343
|
+
React ErrorBoundary. `reset` retries the collected *data sources*, not a UI remount.
|
|
282
344
|
- Refetch: `refresh(results)`. In-flight indicator: `isPending(results)` (or
|
|
283
345
|
`isPending(() => results())`). Freshest in-flight value for a preview:
|
|
284
346
|
`latest(results)`. Do not start `fetch` at component-body top level.
|
|
285
|
-
- Coordinate
|
|
347
|
+
- Coordinate sibling `<Loading>` reveal with `<Reveal>` (`order="sequential"` default,
|
|
348
|
+
or `"together"` / `"natural"`). `collapsed` (sequential only) suppresses tail
|
|
349
|
+
skeletons past the frontier so the page does not stack fallbacks as popcorn.
|
|
286
350
|
|
|
287
351
|
**Never hand-roll these states.** No `[loading, setLoading]` signal, no
|
|
288
352
|
`data() === undefined` branch, no `{ data, error }` signal pair — those are React/Solid 1.x
|
|
@@ -334,21 +398,72 @@ function createSubscriptionQuery<T>(
|
|
|
334
398
|
}
|
|
335
399
|
```
|
|
336
400
|
|
|
401
|
+
Server-function `live()` sources are the same shape (async iterable → memo). Connection
|
|
402
|
+
state is `source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`), not a field
|
|
403
|
+
in the yielded value — see [Server functions](#server-functions--use-server).
|
|
404
|
+
|
|
337
405
|
### Mutations: `action` + optimistic state
|
|
338
406
|
|
|
407
|
+
The synchronous mutation *is* the prediction. Optimistic UI is not a second copy of
|
|
408
|
+
state — it is an overlay the graph discards when the action settles (pass or fail).
|
|
409
|
+
There is no rollback to write. Put in-flight affordances on the record
|
|
410
|
+
(`pending?: boolean`); they vanish with the overlay.
|
|
411
|
+
|
|
339
412
|
```tsx
|
|
340
413
|
import { action, createOptimisticStore, refresh } from 'solid-js';
|
|
341
414
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
415
|
+
type Todo = { id: string; title: string; completed: boolean; pending?: boolean; error?: { completed: boolean } };
|
|
416
|
+
|
|
417
|
+
function createTodos() { // call from a component — not module scope (SSR shares modules)
|
|
418
|
+
const errors = new Map<string, { completed: boolean }>();
|
|
419
|
+
const [todos, setTodos] = createOptimisticStore<Todo[]>(async () => {
|
|
420
|
+
const current = await api.list();
|
|
421
|
+
return current.map((todo) => {
|
|
422
|
+
const error = errors.get(todo.id);
|
|
423
|
+
return error ? { ...todo, error } : todo;
|
|
424
|
+
});
|
|
425
|
+
}, []);
|
|
426
|
+
|
|
427
|
+
const toggleTodo = action(function* (id: string, completed: boolean) {
|
|
428
|
+
setTodos((draft) => {
|
|
429
|
+
const todo = draft.find((row) => row.id === id);
|
|
430
|
+
if (!todo) return;
|
|
431
|
+
todo.completed = completed;
|
|
432
|
+
todo.pending = true;
|
|
433
|
+
});
|
|
434
|
+
try {
|
|
435
|
+
yield api.toggle(id, completed);
|
|
436
|
+
errors.delete(id);
|
|
437
|
+
} catch {
|
|
438
|
+
errors.set(id, { completed });
|
|
439
|
+
} finally {
|
|
440
|
+
refresh(todos);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
return [todos, { toggleTodo }] as const;
|
|
444
|
+
}
|
|
350
445
|
```
|
|
351
446
|
|
|
447
|
+
Three layers, in this order: durable source → ephemeral UI that must survive
|
|
448
|
+
overlay discard (the `errors` map, folded in the projection) → optimistic overlay
|
|
449
|
+
(`pending`, the predicted `completed`). Consumers read one store. Catch
|
|
450
|
+
*expected* mutation failures in the action so a toggle error does not blank the
|
|
451
|
+
list through `<Errored>`; unhandled projection/render errors still reach the
|
|
452
|
+
boundary.
|
|
453
|
+
|
|
454
|
+
Going from a client store to the server is **additive**: same `setTodos`
|
|
455
|
+
calls, wrap each mutation in `action`, swap in `createOptimisticStore` and a
|
|
456
|
+
file of server functions. Do not rewrite `App.tsx` with loading/error branches,
|
|
457
|
+
and do not snapshot-the-cache / write-a-prediction / restore-on-error. A cache
|
|
458
|
+
library is not the productionizing step — the sync mutation already was the
|
|
459
|
+
prediction. (Router `query` or `@tanstack/solid-query` remain valid when the
|
|
460
|
+
project already uses them; they are not required to "make it real".)
|
|
461
|
+
Optimistic rows stay interactive — a not-yet-acked todo can still
|
|
462
|
+
be toggled. Do not disable the control until confirmation, and do not mutex
|
|
463
|
+
rapid clicks; `action` is the transaction (no half-mutation, no interleaved
|
|
464
|
+
list). An in-flight action does not freeze unrelated writes. Collecting failed
|
|
465
|
+
actions to replay is a design choice, not something the runtime requires.
|
|
466
|
+
|
|
352
467
|
Ordinary signal/store writes inside an action are held until it settles. Never call
|
|
353
468
|
`flush()` inside an action. Invoke actions from handlers, not from component/computation bodies.
|
|
354
469
|
Do not set a `submitted` signal and watch it from an effect — that work belongs in the
|
|
@@ -356,11 +471,15 @@ handler or the `action`.
|
|
|
356
471
|
|
|
357
472
|
`createOptimistic` / `createOptimisticStore` are for a tentative value during an
|
|
358
473
|
active mutation, not a local editing session (use a writable derivation or a plain
|
|
359
|
-
store for that).
|
|
474
|
+
store for that). A whole-form `createOptimistic(false)` saving flag is fine; prefer
|
|
475
|
+
`pending` on the record when the affordance is per item. The overlay is
|
|
476
|
+
discarded when the action settles — the confirmed store is the truth. Wrapping
|
|
477
|
+
the same store in server functions does not change that; do not add
|
|
478
|
+
snapshot/restore.
|
|
360
479
|
|
|
361
480
|
**`yield`, not a bare `await`, is the transaction boundary.** `await api.save()` then
|
|
362
|
-
`setTodos(...)` *runs*, but the write commits immediately
|
|
363
|
-
Either `yield saveTodo(todo)` or, when you need a typed result, `const saved = await api.save(); yield; setTodos(...)`.
|
|
481
|
+
`setTodos(...)` *runs*, but the write commits immediately — it is not held as an
|
|
482
|
+
overlay. Either `yield saveTodo(todo)` or, when you need a typed result, `const saved = await api.save(); yield; setTodos(...)`.
|
|
364
483
|
|
|
365
484
|
Navigation-shaped updates do **not** need core `action`. A plain setter is enough:
|
|
366
485
|
reads pull the async, and downstream async computeds hold previous values until the
|
|
@@ -593,6 +712,9 @@ DOM-specific `JSX` types (`JSX.IntrinsicElements`, `JSX.CSSProperties`) and
|
|
|
593
712
|
with `start: true` / `ssr: true` / `serverFunctions: true` only when the
|
|
594
713
|
project already uses start mode. Tiers are `bare` → `basic` (router) →
|
|
595
714
|
`fullstack` (SSR + server functions); do not jump a tier without cause.
|
|
715
|
+
Under `start: true` the plugin owns entries, the document shell, and the build —
|
|
716
|
+
write `src/App.tsx`, not SolidStart-style `entry-client.tsx` / `entry-server.tsx`
|
|
717
|
+
or a hand-written `index.html`.
|
|
596
718
|
|
|
597
719
|
### Solid Router 2 — `createRouter`, not JSX `<Route>`
|
|
598
720
|
|
|
@@ -702,11 +824,21 @@ export async function findUser(id: string) {
|
|
|
702
824
|
```
|
|
703
825
|
|
|
704
826
|
A function-level server function cannot close over component locals — pass
|
|
705
|
-
values as arguments. Treat every argument as untrusted
|
|
827
|
+
values as arguments. Treat every argument as untrusted — TypeScript does not
|
|
828
|
+
cross the HTTP boundary, so do not invent tRPC / RPC type-gen;
|
|
829
|
+
validate inside the function (a parse in the function body is that
|
|
830
|
+
validation, not an RPC schema across the wire). These functions *are* the RPC — do not add an
|
|
831
|
+
API-route file (or a Next `route.ts`) just to wrap `db.todos.insert`. API
|
|
832
|
+
routes are for real HTTP endpoints (webhooks, third-party POST), not for the
|
|
833
|
+
app's own RPC. Read trusted identity
|
|
706
834
|
from `getRequestEvent()`, never from a caller-supplied user id. During SSR the same
|
|
707
|
-
reference runs in-process; in the browser it is HTTP.
|
|
835
|
+
reference runs in-process (no HTTP); in the browser it is HTTP. Anything
|
|
836
|
+
referenced only inside the `"use server"` body (db client, secrets) never
|
|
837
|
+
reaches the client — the directive is the privacy boundary. Mutations that should
|
|
708
838
|
revalidate: `redirect` / `reload` from `@solidjs/web` (see the server-function
|
|
709
|
-
guides).
|
|
839
|
+
guides). After a router/server mutation do **not** follow with a client `fetch`
|
|
840
|
+
to "refresh the page" — `reload` / `revalidate` (and the router's single-flight
|
|
841
|
+
response) close that loop. For 404 during SSR: `httpStatus(404)` from `@solidjs/web`.
|
|
710
842
|
`throw new Error("…")` is stripped to `"Internal Server Error"` in production —
|
|
711
843
|
use `markSafeError` or `respond(..., { status })` for intentional client-facing
|
|
712
844
|
failures. Do not `return Response.json(...)` from `"use server"` — that is
|
|
@@ -715,9 +847,65 @@ caller unwraps. JSON-encodable arguments only unless `enableRichArguments()`
|
|
|
715
847
|
was called once in the client entry (`Date` / `Map` / `Set` throw without it).
|
|
716
848
|
`GET()` is only for idempotent reads (URLs leak into logs/history);
|
|
717
849
|
import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
|
|
850
|
+
Cache GET reads with HTTP, not a hand-rolled Solid cache: the caller still
|
|
851
|
+
receives the value; headers ride the transport.
|
|
852
|
+
|
|
853
|
+
```ts
|
|
854
|
+
import { respond } from '@solidjs/web';
|
|
855
|
+
import { GET } from '@solidjs/web/server-functions';
|
|
856
|
+
|
|
857
|
+
export const fetchStory = GET(async (id: number) => {
|
|
858
|
+
'use server';
|
|
859
|
+
const story = await db.stories.find(id);
|
|
860
|
+
return respond(story, {
|
|
861
|
+
headers: { 'cache-control': 'public, max-age=60, s-maxage=300' },
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
```
|
|
865
|
+
|
|
718
866
|
`live()` yields **current state** (each yield replaces the last); do not treat
|
|
719
|
-
it as an append-only event log
|
|
720
|
-
|
|
867
|
+
it as an append-only event log. Until the first yield it is unsettled
|
|
868
|
+
(`<Loading>`), same as a Promise; later yields are updates — the fallback does
|
|
869
|
+
not return.
|
|
870
|
+
Wire status is a side channel on the **iterable the `live()` call returns**,
|
|
871
|
+
not a field in the value and not a property of the memo:
|
|
872
|
+
|
|
873
|
+
```ts
|
|
874
|
+
const source = stockPrice("ACME");
|
|
875
|
+
source.onstatus = (next) => setStatus(next);
|
|
876
|
+
const price = createMemo(() => source);
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
Do not yield `{ price, connected: true }`. After the first successful connection,
|
|
880
|
+
transient failures retry with exponential backoff; a 4xx is a definite rejection.
|
|
881
|
+
There is no subscription API and no store wiring to configure. For a reactive
|
|
882
|
+
argument, call `live()` inside the memo so the input is tracked:
|
|
883
|
+
`createMemo(() => stockPrice(props.symbol))`. Every reader of that memo shares
|
|
884
|
+
one connection — hoist it instead of calling `live()` at each site. Do not
|
|
885
|
+
subscribe/unsubscribe in the component around `live()`. The `live()` generator
|
|
886
|
+
may still subscribe to an upstream source.
|
|
887
|
+
|
|
888
|
+
**The graph continues across the wire.** An in-flight Promise serializes as a
|
|
889
|
+
Promise and resolves in the client graph as it settles — including before the
|
|
890
|
+
bundle has loaded. A `live()` source sends its first value in the HTML then
|
|
891
|
+
picks up the stream. Do not refetch in `onSettled` / `hydrate` to "load for the
|
|
892
|
+
client", and do not delay `renderToStream` to preserve visual order — `<Reveal>`
|
|
893
|
+
controls when content *appears*, while HTML still ships as soon as it is ready.
|
|
894
|
+
With the router's single-flight integration, a mutation response can seed the
|
|
895
|
+
destination route's preloads; that is the refresh, not a follow-up `fetch`.
|
|
896
|
+
When the app has a server bundle (`ssr` or server functions), production is
|
|
897
|
+
`handleRequest(request)` from the built `dist/server/server.js` (or that
|
|
898
|
+
module's Fetchable default `fetch`) — not an import from `solid-js`. Cloudflare /
|
|
899
|
+
Netlify / Nitro Vite plugins adopt that handler — do not write a Solid adapter,
|
|
900
|
+
a custom Worker, or a Netlify Function. For Node, use the official template
|
|
901
|
+
`server.js` (web `Request` in, stream the response); do not invent an
|
|
902
|
+
Express/Solid bridge. Client-only `start: true` is still static `dist/client`
|
|
903
|
+
— there is no handler to wrap. Request middleware is the Vite option
|
|
904
|
+
`start: { middleware: "./src/middleware.ts" }` (web `Request` + `next`), not
|
|
905
|
+
Express `app.use`. Server functions that return components
|
|
906
|
+
(`serverFunctions: { components: true }`) are experimental preview — do not use
|
|
907
|
+
them unless the project already has that flag on.
|
|
908
|
+
|
|
721
909
|
Unscripted forms: `<form method="post" action={createTodo.url}>` (the reference
|
|
722
910
|
`.url`), not a client `preventDefault` + `fetch`, and not a hand-built
|
|
723
911
|
`/_server/` URL. GET forms (`method="get" action={search.url}`) are only for
|
|
@@ -763,7 +951,7 @@ how to reuse. Prefer the form on the right.
|
|
|
763
951
|
| `setCount(count() + 1)` when writes can batch | `setCount((c) => c + 1)` |
|
|
764
952
|
| `setHandler(fn)` to store a function | `setHandler(() => fn)` (otherwise `fn` is an updater) |
|
|
765
953
|
| `{user() ? <P user={user()!} /> : <SignIn />}` | `<Show when={user()}>{(u) => <P user={u()} />}</Show>` |
|
|
766
|
-
|
|
|
954
|
+
| `const u = user(); return <Child user={u} />` / `user: Promise<User>` / `user: Accessor<User>` as the default | `<Child user={user()} />` and `{props.user.name}` under `<Loading>` — types stay `User`. Pass the memo itself only when the child must `refresh()` that source. `isPending(() => props.user)` works on the value |
|
|
767
955
|
| `lazy(() => import("./p").then((m) => ({ default: m.About })))` | `lazy(() => import("./p"), { export: "About" })` |
|
|
768
956
|
| `dangerouslySetInnerHTML={{ __html }}` | `innerHTML={html()}` (sanitized); never with JSX children |
|
|
769
957
|
| `onClick={setCount}` | `onClick={() => setCount((c) => c + 1)}` |
|
|
@@ -812,6 +1000,17 @@ how to reuse. Prefer the form on the right.
|
|
|
812
1000
|
| `httpStatus(404)` in a click handler | call it bare in the component / error-fallback body (a scope declaration, not a mutation) |
|
|
813
1001
|
| `try { user() } catch (e) { /* NotReadyError */ }` | `<Loading>` — app code should not catch `NotReadyError` |
|
|
814
1002
|
| `{latest(() => results())}` as the visible list | settled read + `isPending()` for the indicator; `latest` is for previews |
|
|
1003
|
+
| `selectedId={latest(selectedId)}` as the default highlight | `selectedId={selectedId()}` so highlight and content stay consistent; `latest` only when the highlight should move first |
|
|
1004
|
+
| `isPending` on a root/global pending signal, or wrapping the whole shell | ask the question at the design site (dimmed list, disabled button, pane). It works on the source, a derived prop, or the write |
|
|
1005
|
+
| `<Loading>` glued to the `createMemo` (consume + block in one place) | fetch high, block low — the boundary wraps the read, not the creation |
|
|
1006
|
+
| `class={{ pending: isPending(id) }}` flashing on every navigation | same class + a short CSS `transition-delay` so only slow swaps show |
|
|
1007
|
+
| `<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}` |
|
|
1008
|
+
| `<Errored>` as a terminal ErrorBoundary; `reset={() => location.reload()}` | the boundary heals when the source succeeds again; `reset` retries collected *sources*, not a UI remount |
|
|
1009
|
+
| `<Errored>` around a list catching a toggle/save failure | catch expected mutation failures in the `action`; keep row errors in a map the projection folds in (survives overlay discard) |
|
|
1010
|
+
| `yield { ...tick, connected: true }` from `live()` | yield the value; wire status is `source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`) |
|
|
1011
|
+
| sibling `<Loading>` fallbacks stacking as popcorn | `<Reveal collapsed>` (sequential default) suppresses tail skeletons past the frontier |
|
|
1012
|
+
| in-memory cache around a GET server function | `return respond(value, { headers: { "cache-control": "..." } })` — metadata rides HTTP, the value rides the graph |
|
|
1013
|
+
| `story()?.title` / treating `story()` as `Story \| undefined` | `{story().title}` under `<Loading>` — if the computation is running, the value is there |
|
|
815
1014
|
| `typeof window !== "undefined"` | `isServer` / `isDev` from `@solidjs/web` |
|
|
816
1015
|
| `Object.assign({}, props)` / expecting `merge` to skip `undefined` like 1.x `mergeProps` | `omit` / `merge` — explicit `undefined` **overrides** (Object.assign) |
|
|
817
1016
|
| `isPending(user())` / `latest(user())` | `isPending(user)` / `latest(user)` — pass the accessor (or `() => user()`). Calling it first evaluates the read before the helper runs |
|
|
@@ -837,6 +1036,16 @@ how to reuse. Prefer the form on the right.
|
|
|
837
1036
|
| `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)` |
|
|
838
1037
|
| `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 |
|
|
839
1038
|
| 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 |
|
|
1039
|
+
| 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 |
|
|
1040
|
+
| 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 |
|
|
1041
|
+
| 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 |
|
|
1042
|
+
| `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 |
|
|
1043
|
+
| 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 |
|
|
1044
|
+
| delay `renderToStream` so HTML arrives in visual order | stream as soon as ready; `<Reveal collapsed>` controls when content *appears* |
|
|
1045
|
+
| 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 |
|
|
1046
|
+
| 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: template `server.js`. Client-only start: static `dist/client`. Middleware is `start: { middleware: "..." }`, not Express `app.use` |
|
|
1047
|
+
| `createMemo(async () => (await props.story).author)` | `createMemo(() => props.story.author)` — a derivation over an async value becomes async; no await, no Promise type |
|
|
1048
|
+
| return a component from `"use server"` / flip `serverFunctions.components` | experimental preview — do not enable unless the project already has that flag |
|
|
840
1049
|
|
|
841
1050
|
```tsx
|
|
842
1051
|
// WRONG — post-await read never subscribes; production can hang pending
|
|
@@ -869,23 +1078,24 @@ function Field(props: { label: string; class?: string }) {
|
|
|
869
1078
|
```
|
|
870
1079
|
|
|
871
1080
|
```tsx
|
|
872
|
-
// WRONG —
|
|
1081
|
+
// WRONG — extracting at the parent is a real read; child's <Loading> never sees it
|
|
873
1082
|
function Page() {
|
|
874
1083
|
const user = createMemo(async () => (await fetch('/api/me')).json());
|
|
875
|
-
|
|
1084
|
+
const current = user();
|
|
1085
|
+
return <Profile user={current} />;
|
|
876
1086
|
}
|
|
877
1087
|
|
|
878
|
-
// CORRECT —
|
|
879
|
-
function Profile(props: { user:
|
|
1088
|
+
// CORRECT — JSX props are lazy (passing isn't reading); types stay User
|
|
1089
|
+
function Profile(props: { user: User }) {
|
|
880
1090
|
return (
|
|
881
1091
|
<Loading fallback={<p>Loading…</p>}>
|
|
882
|
-
<h1>{props.user
|
|
1092
|
+
<h1>{props.user.name}</h1>
|
|
883
1093
|
</Loading>
|
|
884
1094
|
);
|
|
885
1095
|
}
|
|
886
1096
|
function Page() {
|
|
887
1097
|
const user = createMemo(async () => (await fetch('/api/me')).json());
|
|
888
|
-
return <Profile user={user} />;
|
|
1098
|
+
return <Profile user={user()} />;
|
|
889
1099
|
}
|
|
890
1100
|
```
|
|
891
1101
|
|
|
@@ -961,17 +1171,32 @@ always-applied rules installed alongside this skill.
|
|
|
961
1171
|
an effect calling the setter.
|
|
962
1172
|
- [ ] Event handlers wrap setters (`onClick={() => setX(...)}`), use `currentTarget`,
|
|
963
1173
|
and `onInput` for keystrokes. `innerHTML` is sanitized and not mixed with children.
|
|
964
|
-
- [ ] Async memos are passed as
|
|
965
|
-
|
|
1174
|
+
- [ ] Async memos are passed as values (`user={user()}`); JSX props are lazy.
|
|
1175
|
+
Types stay `User` — not `Promise<User>` / `Accessor<User>`. `<Loading>` wraps
|
|
1176
|
+
the *read*, not the memo, not chrome. Extracting `const u = user()` at the
|
|
1177
|
+
parent is the read that throws. Nested child fetches run in parallel
|
|
1178
|
+
(components do not suspend); a real waterfall is `fetchB(a().id)`.
|
|
1179
|
+
`isPending` is per-expression (`isPending(() => props.story)` works);
|
|
1180
|
+
`latest(selectedId)` is opt-in, not the default highlight.
|
|
966
1181
|
- [ ] Lists via `<For>` (server/refetched rows keyed by stable id), conditionals via
|
|
967
1182
|
ternary/`<Show>`; no `{list().map(...)}` in JSX; no `key` props; no `value()!`.
|
|
968
1183
|
- [ ] Effects are two-phase and only at imperative boundaries; apply does not read stores.
|
|
969
1184
|
- [ ] Async reads sit under `<Loading>` (the data slot, not chrome); errors under `<Errored>`.
|
|
970
1185
|
Reactive inputs of an async memo are read before the first `await`. No hand-rolled
|
|
971
1186
|
`loading`/`error` signals or `=== undefined` readiness branches. First load is
|
|
972
|
-
`<Loading>`; refetch indicator is `isPending(user)` (the accessor, not `user()`)
|
|
973
|
-
|
|
974
|
-
|
|
1187
|
+
`<Loading>`; refetch indicator is `isPending(user)` (the accessor, not `user()`)
|
|
1188
|
+
or `isPending(selectedId)` when the write is the question. `<Errored>` heals
|
|
1189
|
+
when the source succeeds; `reset` retries sources, not a UI remount. Per-row
|
|
1190
|
+
mutation failures stay in the action/projection, not in `<Errored>` around
|
|
1191
|
+
the list. Core `refresh(source)`, router `revalidate(key)`, and `return reload(...)` are
|
|
1192
|
+
different APIs — do not mix them. Client → server is additive (same
|
|
1193
|
+
setters + `action` + server functions): do not rewrite App with
|
|
1194
|
+
loading/error branches, disable optimistic rows until ack, or refetch in
|
|
1195
|
+
`hydrate` / `onSettled`. HTTP does not enforce TypeScript — validate
|
|
1196
|
+
inside `"use server"`; do not invent tRPC. `live()` owns the client connection.
|
|
1197
|
+
A server bundle uses `handleRequest` from `dist/server/server.js`; client-only start
|
|
1198
|
+
is static `dist/client`. Do not delay `renderToStream` for visual order
|
|
1199
|
+
(`<Reveal>` does).
|
|
975
1200
|
- [ ] External collections reconcile into stores (function-form `createStore` /
|
|
976
1201
|
`reconcile`), never wholesale draft assignment or `setTodos((t) => t.filter(...))`
|
|
977
1202
|
as the identity-preserving update.
|
|
@@ -72,6 +72,16 @@ read these before writing route JSX.
|
|
|
72
72
|
- https://v2-rebuild--solid-docs-v2.netlify.app/migration/from-solid-router.md
|
|
73
73
|
- https://v2-rebuild--solid-docs-v2.netlify.app/migration/from-solid-meta.md
|
|
74
74
|
|
|
75
|
+
## Design posts (Solid 2 async model)
|
|
76
|
+
|
|
77
|
+
Official Solid 2 blog series. Use these for *why* the colorless/async graph works;
|
|
78
|
+
verify API names against the reference pages above.
|
|
79
|
+
|
|
80
|
+
- https://www.solidjs.com/blog/solid-2-0-rc-the-big-reveal
|
|
81
|
+
- https://www.solidjs.com/blog/async-solid-fetch-high-block-low
|
|
82
|
+
- https://www.solidjs.com/blog/async-solid-write-sync-run-async
|
|
83
|
+
- https://www.solidjs.com/blog/async-solid-one-graph-two-machines
|
|
84
|
+
|
|
75
85
|
## Reference: reactivity (solid-js)
|
|
76
86
|
|
|
77
87
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-signal.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid2-agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Teach AI coding agents (Cursor, Claude Code) to write correct Solid 2.0 instead of React or Solid 1.x — always-applied rules, an agent skill, and a mechanical pattern gate.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|