From 4a24b4252ef730679073e67144ba5d4198491005 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 11 Sep 2026 12:46:47 +0800 Subject: [PATCH] docs: add importable agent skills for using the Solid libraries Three skills under `.claude/skills/`, each a self-contained SKILL.md that an agent picks up on this checkout and that can be copied into another project. They cover usage patterns for people writing Solid apps, not workflows for working on this repository. - `solid-reactivity`: the primitive signatures that changed from Solid 1.x. `createEffect` takes a compute and an effect, a function argument to `createSignal` or `createStore` makes it derived, async is a computation that throws while pending, and the 1.x names that are gone map to their replacements. - `solid-control-flow`: Show, For, Repeat, Switch, Match, Loading, Errored, Dynamic, Portal, and Reveal, with the callback-child and keyed semantics that decide what remounts. - `solid-server-functions`: the closure rule the compiler enforces, why the boundary is not authentication, what declaring GET gives up, error sanitization, and the deployment secret the no-JS form path needs. Every claim is taken from the source in this repository rather than from Solid 1.x, including the removed-API map in `packages/solid/src/index.ts`. Co-Authored-By: Claude Opus 5 --- .claude/skills/solid-control-flow/SKILL.md | 147 ++++++++++++++++++ .claude/skills/solid-reactivity/SKILL.md | 134 ++++++++++++++++ .../skills/solid-server-functions/SKILL.md | 132 ++++++++++++++++ 3 files changed, 413 insertions(+) create mode 100644 .claude/skills/solid-control-flow/SKILL.md create mode 100644 .claude/skills/solid-reactivity/SKILL.md create mode 100644 .claude/skills/solid-server-functions/SKILL.md diff --git a/.claude/skills/solid-control-flow/SKILL.md b/.claude/skills/solid-control-flow/SKILL.md new file mode 100644 index 000000000..6fd5b14cb --- /dev/null +++ b/.claude/skills/solid-control-flow/SKILL.md @@ -0,0 +1,147 @@ +--- +name: solid-control-flow +description: How to use Solid's control-flow components in JSX. Use when writing or reviewing markup that needs conditionals, lists, error handling, or async fallbacks, or that reaches for Show, For, Repeat, Switch, Match, Dynamic, Loading, Errored, Reveal, or Portal. Covers the callback-child and keyed semantics that decide whether content remounts. +--- + +# Solid control flow + +Never use a ternary or `.map()` where one of these fits. They exist so the +runtime can keep DOM across updates instead of rebuilding it. + +## Show + +Renders children when `when` is truthy, otherwise `fallback`. + +```tsx +}> + + +``` + +A function child receives the narrowed value, and `keyed` decides its shape +and its remount behavior. + +```tsx +// Default. The child gets an accessor and is preserved across truthy values. +{u => } + +// Keyed. The child gets the raw value and remounts when identity changes. +{u => } +``` + +The narrowing only holds while the condition is truthy. Reading the accessor +after the block has gone throws. Do not stash it in a timer or an async +continuation. + +## For + +Renders a list. The child is a callback, not markup. + +```tsx +No items}> + {(item, index) =>
{item.label}
} +
+``` + +The `keyed` prop picks which argument is reactive, so it decides what remounts +when the list changes: + +- default or `keyed={true}`: `(item, index)` where `item` is the raw row and + `index` is an accessor. Rows remount when the row value changes. +- `keyed={false}`: `(item, index)` where `item` is an accessor and `index` is + a plain number. Rows survive value changes and update in place. This is what + Solid 1.x called `Index`. +- `keyed={item => item.id}`: both arguments are accessors, and identity is the + returned key. Use this for rows that move. + +`each` accepts `undefined`, `null`, and `false` as an empty list, so a +not-yet-loaded list renders the fallback without a guard. + +## Repeat + +A list from a count rather than from data. + +```tsx +}> + {index => } + +``` + +`from` shifts the starting index. The child may be static markup instead of a +callback. + +## Switch and Match + +Mutually exclusive conditions. The first truthy `Match` wins. + +```tsx +}> + + + + + + + +``` + +`Match` takes the same function-child and `keyed` semantics as `Show`. Only +`Match` elements may be direct children of `Switch`. + +## Loading + +The async boundary. Any read that is still pending throws, and the nearest +`Loading` swaps to its fallback until everything settles. + +```tsx +}> + + +``` + +Scope it around the data-dependent slot, not the surrounding shell. Wrapping +the header, nav, and footer in the same boundary as the data means a +revalidation replaces the whole screen with the fallback. Chrome rendered +outside the boundary stays stable while only the data slot flips. + +The `on` prop scopes the boundary to transitions caused by specific sources, +so writes elsewhere leave the current content up. + +## Errored + +Catches uncaught errors in its subtree. + +```tsx +
Failed: {String(err())}
}> + +
+``` + +The fallback may be markup or a callback taking the error accessor and a +`reset` function. Errors thrown by the fallback itself reach the parent +boundary. + +## Dynamic + +An element or component chosen at runtime. + +```tsx + +``` + +Every other prop is forwarded. Prefer a plain tag when the choice is static, +because a literal tag compiles into the template and a `Dynamic` does not. + +## Portal and Reveal + +`Portal` renders into a different mount point, for overlays and modals. +`Reveal` with `createRevealOrder` controls the order in which sibling async +content appears, replacing Solid 1.x's `SuspenseList`. + +## Compiler note + +These components are auto-imported by the compiler when the name is not +otherwise bound, so a bare `` works without an import. An explicit +`import { Show } from "solid-js"` works too, including under an alias. A local +binding of the same name shadows the built-in and is treated as an ordinary +component. diff --git a/.claude/skills/solid-reactivity/SKILL.md b/.claude/skills/solid-reactivity/SKILL.md new file mode 100644 index 000000000..b4a9079d4 --- /dev/null +++ b/.claude/skills/solid-reactivity/SKILL.md @@ -0,0 +1,134 @@ +--- +name: solid-reactivity +description: How to use Solid 2.0's reactive primitives correctly. Use when writing or reviewing code that calls createSignal, createMemo, createEffect, createStore, createProjection, or createOptimistic, or when async data is involved. Covers the signatures that changed from Solid 1.x and the 1.x APIs that no longer exist. +--- + +# Solid reactivity + +Solid 2.0 changed several signatures that look unchanged. Code written from +Solid 1.x habit compiles and then behaves wrong. Check this file before +writing a primitive call. + +## createEffect takes two functions + +This is the most common mistake. `createEffect` is split into a tracked +compute and an untracked effect. + +```ts +// Correct. +createEffect( + () => count(), // compute: tracks, runs in the compute phase + value => console.log(value) // effect: side effects, runs after the flush +); + +// Wrong. This is the Solid 1.x shape and throws MISSING_EFFECT_FN in dev. +createEffect(() => console.log(count())); +``` + +Return a cleanup from the effect function. It runs before the next run and on +disposal. + +```ts +createEffect( + () => userId(), + id => { + const controller = new AbortController(); + fetch(`/users/${id}`, { signal: controller.signal }); + return () => controller.abort(); + } +); +``` + +Pass `{ effect, error }` instead of a plain function to handle errors that +arrive from the compute or from upstream sources, including async rejections. +Errors thrown inside the effect function itself are not routed there. Wrap +those in `try`/`catch` yourself, because an uncaught one reaches the nearest +error boundary and halts the reactive system if there is none. + +```ts +createEffect(() => user(), { + effect: value => render(value), + error: err => setMessage(String(err)) +}); +``` + +## A function argument makes a signal reactive + +`createSignal` has two shapes. With a value it is a plain signal. With a +function it is a writable memo: it recomputes from its sources, and the setter +applies a local override. + +```ts +const [count, setCount] = createSignal(0); // plain +const [user, setUser] = createSignal(() => fetchUser(userId())); // writable memo + +setUser({ ...user(), name: "Alice" }); // local optimistic edit +``` + +`createStore` works the same way. With an object it is a store. With a +function it is a projection seeded by the second argument. + +```ts +const [state, setState] = createStore({ items: [] }); +const [view, setView] = createStore( + draft => { + draft.total = sum(state.items); + }, + { total: 0 } +); +``` + +## Async is a function that throws while pending + +There is no `createResource` and no `createAsync`. Any computation can be +async: pass a function that returns a promise or async iterable, and a read +while it is pending throws. The nearest `` catches that and shows its +fallback. + +```tsx +const [user] = createSignal(() => fetch(`/users/${id()}`).then(r => r.json())); + +}> + +; +``` + +Do not try to branch on a loading flag inside the computation. Let the read +throw and let the boundary handle it. Use `latest()` to read the previous +settled value instead of suspending, and `isPending()` to check without +triggering. + +Give a computation a `loadingValue` to render a placeholder rather than +suspend. Type the placeholder honestly: if it stands in for real data, mark it +in the data, for example with a `skeleton: true` field, rather than letting it +impersonate a settled value. + +## Solid 1.x APIs that are gone + +Reach for the replacement, not the old name. + +- `createResource` and `createAsync`: any computation is async, see above. +- `batch`: use `flush`. +- `createSelector`: use `createProjection`. +- `createComputed` and `createDeferred`: no replacement, restructure. +- `on`: no longer needed, the compute argument of `createEffect` is the + dependency list. +- `onMount`: use `onSettled`. +- `onError` and `catchError`: use `createErrorBoundary` or ``. +- `startTransition` and `useTransition`: removed. +- `equalFn`: renamed `isEqual`. +- `getListener`: renamed `getObserver`. +- `unwrap`: renamed `snapshot`. +- `createMutable`, `modifyMutable`, `produce`: `produce` behavior is the + default store setter now. +- `indexArray` and `Index`: `` covers both, see the control-flow skill. +- `observable` and `from`: use async iterators. + +The full map with the reasoning is in the `Not Implemented` block at the +bottom of `packages/solid/src/index.ts`. + +## Reading without tracking + +`untrack(fn)` reads without subscribing. Prefer restructuring so the read is +outside the tracked scope; reach for `untrack` when that is not possible. +`snapshot(store)` returns a plain non-reactive copy of a store. diff --git a/.claude/skills/solid-server-functions/SKILL.md b/.claude/skills/solid-server-functions/SKILL.md new file mode 100644 index 000000000..b3172d370 --- /dev/null +++ b/.claude/skills/solid-server-functions/SKILL.md @@ -0,0 +1,132 @@ +--- +name: solid-server-functions +description: How to write and call Solid server functions. Use when adding or reviewing a "use server" function, a form action, a GET-declared read, or code that calls redirect, reload, respond, or markSafeError. Covers the closure rule the compiler enforces, the security expectations of the boundary, and the deployment secret. +--- + +# Solid server functions + +A `"use server"` function runs on the server and is callable from the client +as an ordinary async function. The compiler extracts it, registers it under a +build-stable id, and replaces the client-side reference with a proxy. + +```ts +export async function createPost(title: string) { + "use server"; + return db.post.create({ data: { title } }); +} +``` + +The directive can also sit at the top of a module, which makes the whole +module server-only. + +## The closure rule + +A function-level `"use server"` may only capture module-top-level bindings and +its own parameters. Capturing a variable from an intermediate scope is a +compile error, because the extracted function no longer has that scope. + +```ts +// Rejected at compile time. +function handler(userId: string) { + return async function save(title: string) { + "use server"; + return db.post.create({ data: { title, userId } }); // userId is not top-level + }; +} +``` + +Pass the value as an argument instead. Do not work around the error by hoisting +a mutable module-level variable, because that is shared across concurrent +requests. + +## Treat every server function as a public endpoint + +The boundary is not authentication. Ids are derived from the function name and +the file path with a public algorithm, so they are discoverable, and the +same-origin gate only stops browser-driven cross-site calls. Anything that is +not a browser can present whatever headers it likes. + +Check authorization inside the function, or once for all of them through the +`wrapInvocation` hook. + +```ts +export async function deletePost(id: string) { + "use server"; + const user = await requireUser(); // every call, no exceptions + if (!user.canDelete(id)) throw new Error("forbidden"); + return db.post.delete({ where: { id } }); +} +``` + +## GET declares a read, and gives up the origin gate + +`GET(fn)` makes a function reachable over GET and HEAD so its responses can be +cached. It also skips the same-origin gate, so the function becomes executable +from any origin with the user's cookies. Cross-site code cannot read the +response, but it does cause the function to run. + +Only wrap genuine reads. Never wrap anything that writes, charges, sends, or +is expensive enough to be worth triggering. Deployments that would rather gate +reads than cache them can set `csrf: { protectDeclaredReads: true }`. + +## Returning and throwing responses + +`redirect(url, init)`, `reload(init)`, and `respond(value, init)` build the +control-flow responses. Throw them for early exit, return them for a normal +result. + +```ts +export async function login(form: FormData) { + "use server"; + const session = await authenticate(form); + if (!session) throw new Error("bad credentials"); + throw redirect("/dashboard"); +} +``` + +Validate any redirect target that came from request data. The transport +refuses non-http(s) schemes, so `javascript:` cannot get through, but it does +not restrict the host: a `?next=` parameter passed straight to `redirect` is +an open redirect. + +## Errors are sanitized on the way out + +A plain thrown value is replaced with a generic `Error` outside dev builds, so +a driver error's message, failing query, and connection string do not reach +the client. Send a message deliberately with `markSafeError`. + +```ts +throw markSafeError(new Error("That email is already registered")); +``` + +Only brand messages that are safe for a stranger to read. A branded error +keeps its content, and in a dev build its stack too. + +## Calling with per-call options + +Server function references are called like functions. Use `invoke` when one +call needs an `AbortSignal`, `keepalive`, or a priority hint. + +```ts +const user = await invoke(getUser, { signal: controller.signal }, id); +``` + +Anything with a longer life than one call belongs elsewhere: session-dynamic +headers in the `prepareRequest` hook, declaration-static shape in `GET(fn)` or +`withMeta(fn, meta)`, and retries or deduplication in the data layer that owns +the call. + +## Forms without client JavaScript + +A form can post directly to a server function, and the runtime carries the +outcome to the next render in a one-shot encrypted cookie. That cookie needs a +deployment secret. + +```ts +configureServerFunctionsServer({ secret: process.env.SOLID_SECRET }); +``` + +Use a high-entropy value of 32 bytes or more, shared by every instance behind +the load balancer, and keep it out of source control. The Solid bundler plugin +injects a per-build key when the option is unset. With no key at all the form +still posts and redirects, and only the outcome echo is dropped.