SolidJS Apps skill for Claude Code
Fine-grained reactivity without React reflexes. Claude writes Solid as Solid, not as React with different imports. It keeps every signal read in a tracking scope, never destructures props, swaps early returns for Show and Switch, uses stores with reconcile for server data, and wires SolidStart query, action and server functions with authorization inside each one. It also greps for the patterns that silently freeze values.
- name
solidjs-apps - size188 lines · ≈2,050 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Builds SolidJS and SolidStart apps with fine-grained reactivity used correctly — signals, memos, effects, stores with produce and reconcile, createResource and createAsync, Suspense and ErrorBoundary, Show/For/Index/Switch control flow, Solid Router with query and action, and server functions. Catches React habits that break Solid such as destructuring props, early returns, and calling components like functions. Use when writing .jsx/.tsx for solid-js, porting React components to Solid, or debugging values that stop updating. Try asking
- Port this React component to Solid and keep it reactive
- This value renders once and never updates when the signal changes — why?
- Add a SolidStart route that loads a user with query and saves edits with an action
Use it when
- Writing or reviewing components that import from solid-js
- Porting React code to Solid
- A Solid value stops updating or a list re-renders too much
- Building SolidStart routes, server functions and forms
Skip it when
- The project is React — use react-patterns
- You are choosing where state should live across an app — use state-management first
How to check the result
The build has no hydration warnings, SSR output renders with JavaScript disabled, and a devtools or logging check shows the changed value updating only the nodes that read it.
The common mistake it prevents
Destructuring props in the component signature, which captures the first value and silently stops every later update.
Solid components run once. Reactivity lives in the expressions that read signals, not in re-running functions. Good Solid code keeps every signal read inside JSX, a memo, or an effect; never destructures props; uses control-flow components instead of early returns; and treats stores as the default for nested state.
Step 0 — check versions and setup
npm ls solid-js @solidjs/start @solidjs/router @solidjs/meta
cat app.config.ts vite.config.ts 2>/dev/null | head -40
- solid-js 1.9.x is the stable line in most projects. Solid 2.0 has been in beta/RC
with API changes to async and stores — if
solid-js@2is installed, read its migration notes before applying 1.x patterns here. - SolidStart 1.x (
@solidjs/start) usesapp.config.tswithdefineConfigfrom@solidjs/start/config. Check which config file the project has before editing.
The one mental model
function Counter(props: { step: number }) {
const [count, setCount] = createSignal(0);
console.log("runs once"); // not on every update
const doubled = () => count() * 2; // derived: a plain function is enough
const expensive = createMemo(() => heavy(count())); // memo only when costly or shared
return <button onClick={() => setCount((c) => c + props.step)}>{doubled()}</button>;
}
- A signal getter
count()read inside JSX or a tracking scope subscribes that exact spot. Read it in the component body and you get a frozen snapshot. - Derived values: plain functions first;
createMemowhen the computation is expensive or read in many places. createEffectis for side effects that leave Solid (DOM APIs, logging, storage). Deriving state in an effect and writing it to another signal is a bug pattern.
React habits that break Solid
| React habit | What happens in Solid | Do instead |
|---|---|---|
function C({ name }) destructuring props | Loses reactivity; value frozen at first render | props.name, or splitProps(props, ["name"]) |
if (!data) return <Spinner/> early return | Component never re-evaluates; stuck on spinner | <Show when={data()} fallback={<Spinner/>}> |
items.map(i => <Row/>) | Works but recreates rows on every change | <For each={items()}>{(i) => <Row item={i}/>}</For> |
useEffect deps array | Not a thing; deps tracked automatically | createEffect(() => ...), on(sig, fn) for explicit deps |
useMemo/useCallback everywhere | Unnecessary; functions are created once | Plain functions |
Spreading {...props} into a child after reading | Fine, but reading props early breaks tracking | mergeProps for defaults |
const x = props.value at top | Snapshot | const x = () => props.value |
key prop | Ignored | For keys by reference; Index keys by position |
Default props and forwarding:
function Button(props: ButtonProps) {
const merged = mergeProps({ variant: "primary" }, props);
const [local, rest] = splitProps(merged, ["variant", "children"]);
return <button class={`btn btn-${local.variant}`} {...rest}>{local.children}</button>;
}
Control flow
| Need | Component |
|---|---|
| Conditional | <Show when={user()} fallback={...}>{(u) => <p>{u().name}</p>}</Show> |
| List of objects, identity matters | <For each={rows()}> (item is value, index is signal) |
| List of primitives, position matters | <Index each={tags()}> (item is signal, index is number) |
| Multiple branches | <Switch><Match when={a()}>...</Match></Switch> |
| Async boundary | <Suspense fallback={...}> |
| Error boundary | <ErrorBoundary fallback={(err, reset) => ...}> |
| Render outside tree (modals) | <Portal> |
| Dynamic component | <Dynamic component={comp()} /> |
The Show callback form {(u) => ...} narrows types and keeps the value reactive.
Stores for nested state
import { createStore, produce, reconcile } from "solid-js/store";
const [state, setState] = createStore({ todos: [] as Todo[], filter: "all" });
setState("todos", (t) => t.id === id, "done", (d) => !d); // path syntax
setState(produce((s) => { s.todos.push({ id, title, done: false }); })); // mutable style
setState("todos", reconcile(serverTodos, { key: "id" })); // diff in server data
- Stores track per property: changing
state.todos[3].doneupdates only what reads it. - Never replace a store wholesale with fresh server data — use
reconcileso unchanged rows keep identity and DOM. - Signals for primitives and single values; stores for objects, arrays, and anything nested.
Async data
Plain Solid:
const [user, { refetch, mutate }] = createResource(userId, fetchUser);
// user() undefined while loading, user.loading, user.error; reading user() inside Suspense suspends
SolidStart / Solid Router (preferred in Start apps):
import { query, createAsync, action, revalidate } from "@solidjs/router";
const getUser = query(async (id: string) => {
"use server";
return db.user.findUnique({ where: { id } });
}, "user");
export const route = { preload: ({ params }) => getUser(params.id) };
export default function UserPage(props: RouteSectionProps) {
const user = createAsync(() => getUser(props.params.id));
return <Suspense fallback={<Skeleton />}><h1>{user()?.name}</h1></Suspense>;
}
const renameUser = action(async (form: FormData) => {
"use server";
await requireSession(); // authorize every server function
await db.user.update({ where: { id: String(form.get("id")) }, data: { name: String(form.get("name")) } });
return revalidate(getUser.keyFor(String(form.get("id"))));
});
// <form action={renameUser} method="post">...</form>
querydedupes and caches by key during navigation;preloadstarts fetching on link hover/navigation intent."use server"functions are public HTTP endpoints. Validate input and check authorization inside each one.- Use
useSubmission(action)for pending and error UI on forms.
Routing
- SolidStart file routes live in
src/routes/(index.tsx,[id].tsx,(group)/,[...rest].tsx). API routes exportGET/POSTfrom a route file. - Plain Solid apps:
<Router root={Layout}><Route path="/users/:id" component={User} /></Router>. - Read params reactively:
useParams().idinside JSX or memo, not cached in a const.
Gotchas
- Events:
onClickis delegated;on:clickis native. Useon:for events that must not be delegated (e.g. custom events, stopPropagation interplay with third-party code). classandclassList={{ active: isActive() }}, notclassName.refgives the element directly (let el!: HTMLDivElement; <div ref={el}>); use it inonMount.- Effects created outside a component or root leak: wrap in
createRootand dispose. onCleanupinside effects runs before each re-run and on disposal — clear intervals and listeners there.- Async inside
createEffectloses tracking after the firstawait. Read signals before awaiting. - Libraries built for React do not work. Look for Solid ports (Kobalte, Corvu,
@tanstack/solid-query, @tanstack/solid-table) or wrap vanilla JS libraries in
onMount. - SSR: browser globals only in
onMountorisServerguards fromsolid-js/web.
Verification
npm run buildwith no hydration key warnings;npm run startand load with JS off (SSR content visible) and on.- Use solid-devtools or a temporary
createEffect(() => console.log(x()))to confirm a value actually updates when its source changes. - Grep for frozen-prop patterns before finishing:
grep -rnE "function [A-Z]\w*\(\{|const \{[^}]+\} = props" src --include=*.tsx
grep -rnE "^\s+if \(.*\) return <" src --include=*.tsx
- Type-check with
npx tsc --noEmit.
Report format
**Stack:** solid-js 1.9, @solidjs/start 1.x, router query/action
**Changes:** UserTable → store + reconcile for server rows; removed 3 destructured props
**Server functions:** renameUser (auth checked, input validated, revalidates user key)
**Verified:** build clean, SSR without JS renders, devtools shows granular updates About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.