Skilllibrary solidjs-patterns
Write idiomatic SolidJS code using fine-grained reactivity, control flow components, stores, and resources. Trigger: 'SolidJS component', 'createSignal', 'createStore', 'SolidJS routing', 'reactive state in Solid', 'Solid control flow', '<For>', '<Show>', 'SolidJS Suspense', 'convert React to SolidJS'. Do NOT use for general frontend scaffolding (use frontend-webapp-builder), Tauri-specific integration (use tauri-solidjs), or other frameworks (React, Vue, Svelte).
git clone https://github.com/merceralex397-collab/skilllibrary
T=$(mktemp -d) && git clone --depth=1 https://github.com/merceralex397-collab/skilllibrary "$T" && mkdir -p ~/.claude/skills && cp -r "$T/17-external-reference-seeds/solidjs-patterns" ~/.claude/skills/merceralex397-collab-skilllibrary-solidjs-patterns && rm -rf "$T"
17-external-reference-seeds/solidjs-patterns/SKILL.mdPurpose
Guide the writing of idiomatic SolidJS code — leveraging its fine-grained reactivity model, control flow primitives, store system, and resource management. This skill prevents the most common mistake: writing SolidJS as if it were React.
When to use this skill
Use this skill when:
- writing new SolidJS components, stores, or reactive logic
- debugging reactivity issues (values not updating, effects running unexpectedly)
- converting React components to SolidJS
- choosing between signals, stores, memos, and resources for a given use case
- implementing data fetching with
and SuspensecreateResource - setting up routing with
@solidjs/router - optimizing SolidJS app performance or understanding why "it's already fast"
Do not use this skill when
- the task is general frontend project scaffolding (framework selection, bundler config) — use
frontend-webapp-builder - the task involves Tauri desktop app integration with SolidJS — use
tauri-solidjs - the user is working with React, Vue, or Svelte — those are different reactivity models
- the task is purely CSS/styling with no reactive logic involved
Operating procedure
Step 1 — Understand the reactivity model
SolidJS uses fine-grained reactivity, not virtual DOM diffing. This is the fundamental difference from React:
- React: re-runs the entire component function on state change, diffs the virtual DOM, patches the real DOM.
- SolidJS: component functions run once. Only the specific DOM nodes that depend on changed signals update. There is no re-render.
This means:
- Component functions are setup functions, not render functions.
- Code at the top level of a component runs once during creation, never again.
- Only code inside reactive contexts (
,createEffect
, JSX expressions) tracks and re-executes.createMemo
// This console.log runs ONCE, not on every count change function Counter() { const [count, setCount] = createSignal(0); console.log("Component setup — runs once"); // This JSX expression re-evaluates count() reactively return <button onClick={() => setCount(c => c + 1)}> Count: {count()} </button>; }
Step 2 — Use signals, memos, and effects correctly
Signals — reactive state atoms
import { createSignal } from "solid-js"; const [value, setValue] = createSignal(initialValue); // Read: call the getter function (NOT just reference it) console.log(value()); // ✅ Correct — reads and tracks console.log(value); // ❌ Wrong — references the getter function itself // Write: call the setter setValue(newValue); // Replace setValue(prev => prev + 1); // Update based on previous
Memos — derived reactive values (cached)
Use
createMemo when a derived value is expensive to compute or used in multiple places:
import { createMemo } from "solid-js"; const doubled = createMemo(() => count() * 2); // doubled() is cached — recomputes only when count() changes
Decision rule: if a derivation is just
signal() + 1 used in one place, inline it in JSX. If it's expensive or used in 3+ places, use createMemo.
Effects — side effects that react to state changes
import { createEffect } from "solid-js"; createEffect(() => { // Runs whenever any signal read inside changes console.log("Count changed to:", count()); // Use for: DOM manipulation, logging, localStorage sync, external API calls });
Critical rule: never create signals inside effects. Signals are state; effects are reactions to state. Creating state inside a reaction creates infinite loops or orphaned state.
Step 3 — Handle props correctly
Never destructure props — this is the #1 SolidJS mistake by React developers.
// ❌ WRONG — breaks reactivity. Values are captured at call time, never update. function Greeting({ name, age }) { return <p>Hello {name}, age {age}</p>; } // ✅ CORRECT — access props as properties, reactivity preserved function Greeting(props) { return <p>Hello {props.name}, age {props.age}</p>; }
Use
mergeProps for defaults:
import { mergeProps } from "solid-js"; function Button(props) { const merged = mergeProps({ variant: "primary", size: "md" }, props); return <button class={`btn-${merged.variant} btn-${merged.size}`}> {merged.children} </button>; }
Use
splitProps to separate groups:
import { splitProps } from "solid-js"; function Input(props) { const [local, inputProps] = splitProps(props, ["label", "error"]); return ( <div> <label>{local.label}</label> <input {...inputProps} /> <Show when={local.error}><span class="error">{local.error}</span></Show> </div> ); }
Step 4 — Use control flow components (not JS expressions)
SolidJS provides control flow components that preserve reactivity. Using JavaScript ternaries or
Array.map breaks the reactive model.
<Show>
— conditional rendering
<Show>import { Show } from "solid-js"; // ✅ Correct — reactive, preserves DOM nodes when condition is stable <Show when={isLoggedIn()} fallback={<LoginForm />}> <Dashboard /> </Show> // ❌ Wrong — ternary recreates DOM nodes on every change {isLoggedIn() ? <Dashboard /> : <LoginForm />}
<For>
— reactive list rendering
<For>import { For } from "solid-js"; // ✅ Correct — keyed by reference, only updates changed items <For each={items()}> {(item, index) => <li>{index()}: {item.name}</li>} </For> // ❌ Wrong — Array.map recreates all DOM nodes on any list change {items().map((item, i) => <li>{i}: {item.name}</li>)}
Use
<Index> when items are primitives (strings, numbers) and the array position is the stable identity:
import { Index } from "solid-js"; <Index each={names()}> {(name, index) => <li>{index}: {name()}</li>} {/* note: name is a signal */} </Index>
<Switch>
/ <Match>
— multi-branch conditional
<Switch><Match>import { Switch, Match } from "solid-js"; <Switch fallback={<p>Unknown status</p>}> <Match when={status() === "loading"}><Spinner /></Match> <Match when={status() === "error"}><ErrorDisplay /></Match> <Match when={status() === "success"}><DataView /></Match> </Switch>
<Dynamic>
— dynamic component selection
<Dynamic>import { Dynamic } from "solid-js/web"; const components = { home: HomePage, about: AboutPage, settings: SettingsPage }; <Dynamic component={components[currentPage()]} />
Step 5 — Manage complex state with stores
Use
createStore for nested/complex state (objects, arrays of objects):
import { createStore } from "solid-js/store"; const [state, setState] = createStore({ user: { name: "Alice", settings: { theme: "dark" } }, todos: [ { id: 1, text: "Learn SolidJS", done: false }, { id: 2, text: "Build app", done: false }, ], }); // Path-based updates — only affected DOM nodes re-render setState("user", "settings", "theme", "light"); setState("todos", todo => todo.id === 1, "done", true); // Batch complex updates with produce (Immer-like API) import { produce } from "solid-js/store"; setState(produce(s => { s.todos.push({ id: 3, text: "Deploy", done: false }); s.user.name = "Bob"; })); // Replace store data from server with reconcile (structural diffing) import { reconcile } from "solid-js/store"; const serverData = await fetchTodos(); setState("todos", reconcile(serverData));
Decision rule for signals vs stores:
- Single value, flat data →
createSignal - Nested object, array of objects, frequently partially updated →
createStore
Step 6 — Fetch data with resources
createResource is SolidJS's built-in async data primitive:
import { createResource, Suspense, ErrorBoundary } from "solid-js"; const fetchUser = async (id: string) => { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error(`Failed to fetch user ${id}`); return res.json(); }; function UserProfile() { const [userId] = createSignal("123"); const [user, { refetch, mutate }] = createResource(userId, fetchUser); return ( <ErrorBoundary fallback={(err) => <p>Error: {err.message}</p>}> <Suspense fallback={<Spinner />}> <h1>{user()?.name}</h1> <button onClick={refetch}>Refresh</button> </Suspense> </ErrorBoundary> ); }
Key patterns:
- First argument (source signal) triggers refetch when it changes.
updates the cached value optimistically without refetching.mutate
forces a new fetch.refetch- Always wrap resource consumers in
and<Suspense>
.<ErrorBoundary>
Step 7 — Set up routing
Use
@solidjs/router:
import { Router, Route, A } from "@solidjs/router"; import { lazy } from "solid-js"; const Home = lazy(() => import("./pages/Home")); const About = lazy(() => import("./pages/About")); const UserProfile = lazy(() => import("./pages/UserProfile")); function App() { return ( <Router> <Route path="/" component={Home} /> <Route path="/about" component={About} /> <Route path="/users/:id" component={UserProfile} /> </Router> ); }
Route data loading:
import { useParams } from "@solidjs/router"; function UserProfile() { const params = useParams<{ id: string }>(); const [user] = createResource(() => params.id, fetchUser); // params.id is reactive — changing the route refetches automatically }
Step 8 — Use context for dependency injection
import { createContext, useContext } from "solid-js"; interface AuthContextType { user: () => User | null; login: (creds: Credentials) => Promise<void>; logout: () => void; } const AuthContext = createContext<AuthContextType>(); export function AuthProvider(props) { const [user, setUser] = createSignal<User | null>(null); const value: AuthContextType = { user, login: async (creds) => { /* ... */ }, logout: () => setUser(null), }; return ( <AuthContext.Provider value={value}> {props.children} </AuthContext.Provider> ); } export function useAuth() { const ctx = useContext(AuthContext); if (!ctx) throw new Error("useAuth must be used within AuthProvider"); return ctx; }
Step 9 — Performance considerations
SolidJS is fast by default because there are no re-renders. Optimization is mostly about not fighting the framework:
for code splitting — same as React.lazy but for SolidJS components.lazy()
for non-urgent updates — keeps the UI responsive during heavy state changes.startTransition
— groups multiple signal updates into one reactive flush (usually automatic, but useful in async callbacks).batch()
— explicitly declare which signals an effect depends on (prevents over-tracking).on()
import { on, createEffect } from "solid-js"; // Only reacts to count changes, NOT to name changes even if name() is read createEffect(on(count, (value) => { console.log("Count is now:", value, "Name is:", name()); }));
Decision rules
- Never destructure props — this is non-negotiable. Use
for defaults,mergeProps
for separation.splitProps - Use control flow components, not JS expressions —
over ternaries,<Show>
over<For>
. The built-in components maintain reactive tracking.Array.map - Signals for flat state, stores for nested state — if you're putting an object in a signal and spreading it to update, switch to
.createStore - Component functions are constructors, not render functions — any code you want to re-run must be inside a reactive context (effect, memo, JSX expression).
- Wrap every
in Suspense + ErrorBoundary — unhandled async errors and loading states crash the app silently.createResource - Use
for server data,reconcile()
for local mutations — don't replace entire store state for partial updates.produce() - Prefer
for effects with specific dependencies — prevents accidental over-tracking when an effect reads signals it shouldn't react to.on()
Output requirements
Produce a structured deliverable with these sections:
- Component Definition — component code with proper props handling (no destructuring), typed props interface,
/mergeProps
usage where applicable.splitProps - Store Architecture — store shape definition, update patterns (path-based,
,produce
), which components own vs consume each store.reconcile - Route Configuration — route tree with lazy-loaded components, route data functions, parameter handling, layout nesting.
- Resource Pattern —
setup with source signals, Suspense/ErrorBoundary wrapping, optimistic updates viacreateResource
, refetch triggers.mutate - Reactivity Map — diagram of signal → memo → effect chains showing what depends on what and which DOM nodes update when each signal changes.
Anti-patterns
- Destructuring props:
breaks reactivity permanently. The value is captured once at component creation and never updates. Always accessfunction Comp({ name })
.props.name - Using
instead ofArray.map
: recreates all DOM nodes on every list change instead of surgically updating only changed items. Causes layout thrashing and lost component state.<For> - Creating signals inside effects:
creates a new signal on every effect run, leaking memory and causing unpredictable behavior.createEffect(() => { const [x, setX] = createSignal(0); }) - Treating SolidJS like React: expecting component functions to re-run, using
/useMemo
equivalents (unnecessary — there are no re-renders to optimize away), reaching foruseCallback
patterns.React.memo - Storing derived values in signals:
should becreateEffect(() => setDoubled(count() * 2))
. Signals are for source state; memos are for derived state.const doubled = createMemo(() => count() * 2) - Forgetting that signal getters are functions: writing
in JSX instead of{count}
renders the function reference, not the value.{count()} - Over-nesting Suspense boundaries: one Suspense per resource causes layout shift cascades. Group related resources under one Suspense boundary for cohesive loading states.
- Mutating store state directly:
bypasses reactivity. Always usestate.user.name = "Bob"
orsetState
.produce
Related skills
— project scaffolding and framework selection (use when starting a new project)frontend-webapp-builder
— Tauri desktop app integration with SolidJStauri-solidjs
— backend API patterns (SolidJS frontend + FastAPI backend is a common pairing)fastapi-patterns
Failure handling
- If reactivity is broken (values not updating), check for prop destructuring first — it's the cause >50% of the time.
- If effects run in infinite loops, check for signals being created or set inside effects. Use
to explicitly scope dependencies.on() - If converting from React, don't translate line by line. Rewrite from the component's purpose: what signals does it own, what does it derive, what side effects does it perform.
- If store updates aren't reflected in the UI, ensure you're using path-based
orsetState
— not direct mutation of the store object.produce - If the team is more comfortable with React patterns, consider whether SolidJS is the right choice for the project — skill misalignment costs more than framework performance gains.