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.

  • namesolidjs-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

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.

~/.claude/skills/solidjs-apps/SKILL.md

--- name: solidjs-apps description: Builds SolidJS and SolidStart apps with fine-grained reactivity used correctly — signals, memos, effects, stores with produce and… license: MIT ---

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@2 is installed, read its migration notes before applying 1.x patterns here.
  • SolidStart 1.x (@solidjs/start) uses app.config.ts with defineConfig from @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; createMemo when the computation is expensive or read in many places.
  • createEffect is 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 habitWhat happens in SolidDo instead
function C({ name }) destructuring propsLoses reactivity; value frozen at first renderprops.name, or splitProps(props, ["name"])
if (!data) return <Spinner/> early returnComponent 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 arrayNot a thing; deps tracked automaticallycreateEffect(() => ...), on(sig, fn) for explicit deps
useMemo/useCallback everywhereUnnecessary; functions are created oncePlain functions
Spreading {...props} into a child after readingFine, but reading props early breaks trackingmergeProps for defaults
const x = props.value at topSnapshotconst x = () => props.value
key propIgnoredFor 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

NeedComponent
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].done updates only what reads it.
  • Never replace a store wholesale with fresh server data — use reconcile so 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>
  • query dedupes and caches by key during navigation; preload starts 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 export GET/POST from a route file.
  • Plain Solid apps: <Router root={Layout}><Route path="/users/:id" component={User} /></Router>.
  • Read params reactively: useParams().id inside JSX or memo, not cached in a const.

Gotchas

  • Events: onClick is delegated; on:click is native. Use on: for events that must not be delegated (e.g. custom events, stopPropagation interplay with third-party code).
  • class and classList={{ active: isActive() }}, not className.
  • ref gives the element directly (let el!: HTMLDivElement; <div ref={el}>); use it in onMount.
  • Effects created outside a component or root leak: wrap in createRoot and dispose.
  • onCleanup inside effects runs before each re-run and on disposal — clear intervals and listeners there.
  • Async inside createEffect loses tracking after the first await. 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 onMount or isServer guards from solid-js/web.

Verification

  1. npm run build with no hydration key warnings; npm run start and load with JS off (SSR content visible) and on.
  2. Use solid-devtools or a temporary createEffect(() => console.log(x())) to confirm a value actually updates when its source changes.
  3. 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
  1. 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.

esc